---
title: "Host Bridge — Secrets and Approved Host Actions"
---
import { Aside } from '@astrojs/starlight/components'
import RepoFile from '../../../../components/RepoFile.astro'


**Status**: Open — design proposal

## Problem

jackin' isolates agent containers from the host on purpose. That's the whole security model. But hard isolation creates two operational pain points the operator runs into often:

1. **An agent needs a secret for one command, mid-session.** The credential wasn't on the operator's pre-launch env-var allowlist (it's a one-off — a database password the operator has in 1Password, an SSH passphrase for a single push, an API key for a tool the agent is about to invoke). Today the operator's choices are bad: stop the agent, edit the workspace env vars, restart the container, lose the agent's working state — or paste the secret into the agent's prompt and let the agent see the raw value forever.
2. **An agent needs to do something only the host can do.** Examples: read a TouchID-protected Keychain entry, sign a release with the host's GPG key (which jackin' deliberately doesn't forward), invoke a CLI that's licensed only on the host, run `gh pr merge` against a repo whose write token lives in the host's Keychain. Today the operator has to leave the agent's flow, run the command themselves, paste the output back. Every interruption costs flow.

Both look like the same shape from the operator's seat: *"the agent needs a single piece of host help, and I want to authorize it without tearing down the session."*

This roadmap item proposes a **host bridge** — a small request/approval channel between agents inside containers and the operator on the host, hosted by the [jackin daemon](/reference/roadmap/jackin-daemon/). Agents request a secret value or a host command; the operator gets a host-side prompt (touch confirmation, biometric, password); on approval the daemon does the work; the agent receives back exactly what it asked for and nothing more.

The daemon owns the policy, audit log, and execution path. The Jackin Desktop
Agent Hub is the preferred native macOS surface for viewing pending requests and
audit state once it exists; agents still talk to the daemon, not directly to the
app.

## Two flows

The bridge serves two distinct flows that share a transport, an approval surface, and an audit log.

### Flow 1 — secret-on-demand

The agent inside the container realizes it needs a secret it doesn't have. It calls a tool on a jackin'-provided MCP server:

```
secret.request(name: "POSTGRES_PROD_PASSWORD", scope: "single-command", reason: "running pg_dump")
```

The daemon receives the request and dispatches it to the operator: a host-side prompt naming the workspace, role, agent, the secret name, the operator-supplied reason, and the requested scope. The operator confirms with whatever auth the host supports — TouchID on macOS, polkit / pkcheck on Linux, password fallback. On approval, the daemon resolves the secret from the operator's chosen source (1Password, host Keychain, environment) and returns the value into the container.

Crucially, **the agent does not see the resolved value as a string it can echo, log, or tool-output.** The MCP server hands the secret to the agent's runtime as an opaque handle, and the runtime substitutes the handle into the next single command. The handle is single-use and expires immediately after the command exits — the value never lands in the agent's context window or chat history.

Three scope options:

- `single-command` — one substitution, then the handle is invalidated.
- `session` — the secret stays usable for the duration of the agent's current session, but never persists past restart.
- `persisted` — the operator approves writing the secret to the workspace's env-vars layer so subsequent launches inherit it. Identical effect to manually editing the workspace env config; the bridge just spares the operator the workspace-editor round-trip.

Default is `single-command`. The other two require explicit operator selection on the prompt.

### Flow 2 — operator-approved host command

The agent realizes it needs an action only the host can perform. It calls:

```
host.run(command: "gh pr merge 123 --squash", reason: "shipping the auth PR after CI")
```

The daemon dispatches a host-side prompt naming the workspace / role / agent / command / reason, with the same TouchID / polkit / password approval. On approval, the daemon runs the command on the host — same UID as the operator — captures stdout / stderr / exit code, and returns those back to the agent.

The agent does not get *shell access* to the host. Each `host.run` call is one-shot, one command, fully recorded.

Approval policy is per-workspace and per-role:

- `always-prompt` (default) — every `host.run` call hits the operator approval flow.
- `allowlist` — operator pre-declares a set of command patterns the daemon can run without prompting (e.g. `gh pr view *`, `gh api user`). Calls outside the allowlist still prompt.
- `blocklist` — symmetric: pre-declared denied patterns are rejected without prompting; everything else prompts.
- `disabled` — `host.run` is unavailable for this workspace / role. The MCP tool isn't even registered.

`disabled` is the policy shipped to roles that have no business asking the host to run anything (read-only review roles, scratch experimentation roles).

## Why this is one item, two flows

The two flows look superficially different — one returns a secret value, the other runs a command — but the daemon-side machinery is identical:

- Same MCP server inside the container (different tool surfaces).
- Same control channel between MCP server and daemon.
- Same operator-approval prompt shape (workspace / role / agent / what-is-being-asked / reason / approve-or-deny).
- Same biometric / password / touch-confirmation backend.
- Same audit log.
- Same `disabled` policy escape hatch.

Splitting the two flows into two roadmap items would multiply the security model, the operator UX, and the daemon code paths for no benefit. Keep them together.

## Operator approval surface

The host-side prompt is the load-bearing part of this design. It must be:

- **Hard to spoof.** A real operator-presence proof — TouchID, system polkit, sudo-style password fallback — not just a click-through dialog any agent could simulate by running a fake one inside the container. The container has no path to spawn a host-process; the prompt comes from the daemon, which has process-level integrity.
- **Stateful per request.** Each request gets its own prompt with the request ID baked in. Approving prompt A does not approve a different request B that arrives a millisecond later.
- **Auditable.** Every prompt + response (approved / denied / timed out) lands in the daemon's audit log with timestamp, workspace, role, agent, request payload (with secrets redacted), and the operator's response. Reviewable via `jackin audit` (subcommand to be designed).
- **Time-bounded.** Prompts expire after a configurable window (default 30s). Expiry counts as deny. The agent receives a structured "operator did not approve in time" error.
- **Visually distinct.** The prompt is a system-modal notification or native
  Desktop approval surface, not a TUI overlay — agent presence in jackin'
  console must not block another agent's approval prompt from reaching the
  operator.

Per-platform implementation:

- **macOS.** `LocalAuthentication` framework — TouchID where available, falls back to login password. Wrapped by a small Swift helper bundled with the daemon.
- **Linux.** `polkit` for the password / fingerprint flow on systems with PAM-fingerprint configured. Falls back to a sudo-style terminal password prompt for headless setups.
- **Windows.** Out of scope until jackin' supports Windows hosts.

## Threat model

This feature widens the attack surface jackin' presents to a malicious agent. The threat model has to be explicit before the implementation lands.

### What a malicious agent can do today (without this feature)

- Run any command inside its own container (full speed by design — that's the trust boundary).
- Read host filesystem only via mounts the operator chose.
- Access network only via the container's network policy.
- Cannot reach the host's process namespace, Keychain, file system outside mounts, or invoke host CLIs.

### What this feature changes

- Agents gain a *request* channel to the host. Without operator approval, every request fails. With approval, the agent gets exactly what was approved and nothing more.
- An agent that gets clever about spamming requests cannot bypass approval — every request hits the operator's prompt or is rejected by policy.
- An agent that gets clever about *social engineering* the operator (asking for a more dangerous action than necessary, hiding intent in a benign-looking request) is the real risk. Mitigation: the prompt shows exactly what's being requested; the audit log captures everything; allowlist policies let operators pre-decide low-risk patterns instead of approving each one individually under decision fatigue.

### What stays out of scope

- Agents cannot pre-position state on the host (no "create a file at /tmp/X for me" — that's just a `host.run` call with a write-flavored command, subject to the same policy).
- Agents cannot escalate privilege (the daemon runs as the operator's UID; commands it runs inherit that UID).
- Agents cannot interact with another agent's container directly through the bridge — every `host.run` call still goes through the operator.

### Defense-in-depth

- **Per-workspace `disabled` default for high-risk roles.** Roles that don't need the bridge get it disabled at the policy layer; the MCP tool isn't even registered.
- **Allowlist patterns.** Operators can pre-declare safe command patterns. The default is no allowlist — every request prompts.
- **Audit log retention.** Approval / deny events kept for at least 90 days, exportable via `jackin audit export`.
- **Prompt-fatigue debounce.** When the operator denies the same request shape 3+ times in a row, the daemon offers to add an explicit deny rule rather than continuing to prompt.

## How the agent expresses requests

Same shape as [agent attention prompts](/reference/roadmap/agent-attention-prompts/): a jackin'-bundled MCP server, auto-registered at container launch, exposes the `secret.*` and `host.*` tool families. The agent runtime's tool-use loop calls these tools when the agent decides it needs a secret or a host command; jackin' bundles the MCP server, registers it in each agent's MCP config — extending the registration pattern <RepoFile path="docker/runtime/entrypoint.sh" /> uses today for the Claude runtime's `tirith` / `shellfirm` to every supported runtime — and never asks the operator to wire it manually.

Tool surface (initial cut):

- `secret.request(name, scope, reason)` → opaque handle.
- `secret.use_in(template_string)` → runs the next command with the handle's value substituted; the value never appears in tool output the agent can read.
- `host.run(command, reason)` → returns `{stdout, stderr, exit_code}` after operator approval.
- `host.run_capability(command_pattern)` → returns whether `command_pattern` would be allowed by the current workspace policy; lets the agent ask "before I prompt the operator, can I even do this?" without burning a prompt cycle.

## Why the daemon

Same shape as the other reactive features that depend on it. The bridge needs:

- A long-running host process that the container's MCP server can call (per-container Unix domain socket through the bind-mount channel — see [jackin daemon](/reference/roadmap/jackin-daemon/) for the channel design).
- A host-side process that has integrity to display approval prompts the operator trusts.
- A central audit log that survives across CLI invocations and individual container lifetimes.
- A policy engine (allowlist / blocklist / `disabled`) configured per workspace and queryable from the MCP server.

None of these can run from a CLI that exits between commands. The daemon's lifecycle, install, security posture, and control-socket transport are decided in the [jackin daemon](/reference/roadmap/jackin-daemon/) item; this feature is one adapter against that base.

## Host-side effects

Per <RepoFile path="AGENTS.md" /> "Never mutate the host machine
silently": the bridge runs operator-authorized commands as the
operator's UID and writes to host-side audit state. Every effect
is gated on a TouchID / polkit / password approval surface that
names the request before it runs.

- **Arbitrary host commands.** `host.run` invokes a command on
  the host with the operator's UID, only after operator
  approval through the daemon's prompt surface
  (`LocalAuthentication` / polkit / password). The command, the
  agent's stated reason, and the approval/deny outcome land in
  the audit log. Per-workspace `disabled` / `allowlist` /
  `blocklist` policies bound what can ever be requested.
- **Secret reads from operator-chosen sources.**
  `secret.request` resolves through whatever store the operator
  configured (Keychain, 1Password, env, etc.). Reads happen only
  on operator approval; the daemon never enumerates secrets and
  never caches values past the lifetime of the single
  `secret.use_in` substitution.
- **Audit log writes.** The daemon appends approval / deny
  events to `~/.jackin/log/host-bridge.jsonl` (path subject to
  daemon design). No other host file is mutated by this feature.
- **Opt-out path.** Setting the workspace policy to `disabled`
  unregisters the MCP server inside the container; no further
  requests are even surfaced to the operator.
- **Out of scope: silent host writes.** This feature deliberately
  ships no shape that can mutate the host without per-request
  approval. Pre-approved allowlist patterns still surface a
  one-line audit-log entry per invocation; no command runs
  invisibly.

## Phase 1 cut

A minimal slice that proves the shape:

1. MCP server with just `host.run` (the simpler of the two flows from a "no special handle plumbing" perspective). `always-prompt` policy only — no allowlist / blocklist yet.
2. macOS approval surface via `LocalAuthentication` (TouchID + password fallback).
3. Linux approval surface via terminal password prompt (no polkit yet).
4. Audit log written to `~/.jackin/log/host-bridge.jsonl` with rotation.
5. `Bridge` tab in `jackin console` workspace editor (parallel to the existing General / Mounts / Roles / Secrets / Auth tabs): enable / disable per workspace, view recent audit entries.

Not in phase 1: `secret.request` / `secret.use_in` (more complex handle lifecycle), allowlist / blocklist policies, polkit integration, `host.run_capability`, prompt-fatigue debounce, audit export tooling.

## Open design questions

### Secret-handle lifecycle

How does the MCP server hand the agent a value that the agent can use *in a command* but cannot read *as a string*? Possible mechanisms:

- **Wrapper command.** The MCP server returns an opaque handle; the agent calls `secret.use_in("DATABASE_URL=$HANDLE pg_dump …")` and the MCP server sets up the env var on the next process the runtime spawns. Requires runtime cooperation.
- **Pipe injection.** The MCP server attaches the value to a fd handed to the next command via the agent runtime's process spawn API. Strictest separation but most fragile across runtimes.
- **Heredoc redaction.** Agent runtime captures stdout / stderr; the MCP server marks the handle's value as "redact in capture" so it appears in the running process but not in the conversation log. Easiest to implement but leaks if the value reaches stdout via an unrelated subprocess.

The first option is probably the right fit — explicit, runtime-mediated, hard to misuse. Pinning down the protocol is the open question.

### Approval-prompt UX

A system modal the operator sees on every request will annoy them very fast. Strategies for sustainable UX:

- **Bundling.** When two requests arrive within a few seconds, fold them into one prompt that lists both.
- **Allowlist learning.** After the operator has approved the same exact command pattern 5 times, offer to add it to the workspace's allowlist with a "stop asking me about this" check.
- **Recent-history view.** A small TUI overlay (`jackin console` Bridge tab, also reachable from a global hotkey) showing pending and recent requests so the operator can review without losing context.

### Cross-agent reuse

When two agents in two containers both need the same secret, does the operator have to approve twice? Caching the approved-secret-handle for a configurable window (10 minutes?) would dedupe the prompt, but introduces a state question (which agent gets to "use" the cached handle first?). Phase 1 punts on this — no caching, every request prompts — and revisits when usage data shows the prompt rate is the bottleneck.

### Cross-host

When jackin' grows to remote agent-host architectures (Kubernetes phase, see the [Why jackin'](/getting-started/why/) page), the bridge needs to reach the operator wherever they are. Out of scope for this item; revisit when remote-agent work lands.

## Why this matters

The pitch of jackin' is *"running many agents safely is a productivity win."* The single biggest threat to that pitch is friction: every time the operator has to leave the agent's flow to do a host-side action, the agent stalls and the operator's flow breaks. Moving from *"stop the agent, do the host thing yourself, paste back"* to *"agent asks, you press TouchID, agent continues"* is the difference between agents being a sometime-help and agents being a workflow.

The mechanism is also the right escape valve for the *"help, I forgot to set up X before launching"* class of problems. Without it, every gap in pre-launch config is a session-restart penalty. With it, the operator unblocks the agent in three seconds and the work continues.

## Related work

- [jackin daemon](/reference/roadmap/jackin-daemon/) — host-side process this feature plugs into. Phase 1 daemon must ship before this adapter can.
- [Live bidirectional auth sync](/reference/roadmap/live-auth-sync/) — sibling feature plugging into the same daemon; sets the precedent for per-axis adapters.
- [Agent attention prompts](/reference/roadmap/agent-attention-prompts/) — sibling MCP-server feature using the same auto-registration pattern; both flow through the daemon.
- [Jackin Desktop Agent Hub](/reference/roadmap/jackin-desktop-agent-hub/) — native macOS surface that should eventually show host-bridge approvals and audit entries while keeping daemon policy/execution as the source of truth.
- [Container credential exposure — beyond env injection](/reference/roadmap/container-credential-exposure/) — captures the broader trade-off between today's `docker run -e` env injection and the daemon-mediated `secret.request` flow this item proposes; together they replace env-resident tokens with on-demand handles.
- [Credential proxy](/reference/roadmap/) — earlier-roadmap idea about proxy-based credential injection without storing tokens inside containers; this item is the operator-mediated counterpart for secrets the operator wants to vet per use.
- <RepoFile path="docker/runtime/entrypoint.sh" /> — current MCP-registration pattern for `tirith` / `shellfirm`. The bridge MCP server registers through the same code path.
