A worker now opens the same directory the primary is in, unless told otherwise.
Resolution: explicit spawn cwd → per-profile config cwd → the primary's cwd
(auto-detected from the bridge_spawn caller's PID via lsof -d cwd) → the daemon's
cwd. Never $HOME.
Mechanism (found by live probe, corrects the earlier assumption): an agent.start
pane does NOT inherit its tab's or workspace's cwd — it starts in $HOME. herdr's
agent.start honours an (undocumented) cwd param, so the resolved cwd is threaded
onto agent.start {cwd} (both tab and pane placement), not tab.create.
Surfaces: bridge_spawn {cwd?} + auto-detect via ConnectionIdentity.resolve (peer
PID) + ProcessCwdLookup (lsof); REST POST /workers ?cwd= / body cwd; per-profile
'cwd:' config. Validated live: explicit cwd → worker rooted there; no cwd over
REST → daemon cwd, not $HOME. Also clears the ccs folder-trust prompt when the
project dir is already trusted (see docs/Worker-Startup-and-Trust.md).
7.1 KiB
Worker startup: working directory & the folder-trust prompt
When bridged spawns a worker, the worker CLI may show an interactive startup prompt before it
is ready to accept a task — most importantly a "Do you trust the files in this folder?" dialog. An
unattended worker parked on that prompt never becomes injectable: the status-gated injector waits for
idle/blocked, the task is never delivered, and (worst case) a stray Enter answers the dialog
wrong. How this is handled depends on two things:
- The worker's working directory — which folder the CLI is asked to trust.
- Which CLI launches the worker — each has its own first-run/trust behaviour.
This doc pins the current assumption (ccs is the launcher), how its trust prompt works, the
rule that a worker inherits the primary's directory (never $HOME), and how other CLIs differ.
flowchart TD
A["bridge_spawn / POST /workers"] --> B{"explicit cwd?<br/>(profile cwd or spawn arg)"}
B -->|"yes — told otherwise"| C["use that cwd"]
B -->|"no"| D{"caller PID resolvable?<br/>(MCP peer PID)"}
D -->|"yes"| E["cwd = the primary's cwd<br/>lsof -a -p PID -d cwd"]
D -->|"no (REST / off-host)"| F["cwd = bridged daemon cwd<br/>(never $HOME by assumption)"]
C --> G["ensureWorkspace → tab.create → agent.start {cwd}"]
E --> G
F --> G
G --> H{"does the CLI trust this folder?"}
H -->|"yes"| I["worker reaches its prompt → injectable"]
H -->|"no"| J["worker BLOCKS on the trust dialog<br/>never injectable"]
classDef good fill:#2f855a,stroke:#22543d,color:#ffffff;
classDef bad fill:#9b2c2c,stroke:#63171b,color:#ffffff;
class I good
class J bad
Figure 1 — spawn resolves a working directory, then the CLI's trust check gates readiness.
Working directory: inherit the primary's path
Rule: a worker opens the same directory the primary (main) session is working in, unless told
otherwise. Never assume $HOME. If the primary is in /Users/you/LTMS/claude-bridge, its workers
open there too — so delegated tasks share the same relative paths and the same (already-trusted)
project folder.
The herdr seam. An agent.start pane does not inherit its tab's or workspace's cwd — it
starts in $HOME unless told otherwise. herdr's agent.start honours an (undocumented) cwd
param, verified live: setting it roots the worker process at that directory. So the worker's cwd is
threaded onto agent.start {…, cwd}, not the placement step (workspace.create/tab.create cwd
only affect the seed shell, which the bridge closes).
Resolution order (first match wins):
| # | Source | When |
|---|---|---|
| 1 | Explicit cwd — a per-profile cwd: in config, or a spawn argument |
"told otherwise" — pin a fixed workdir |
| 2 | The primary's cwd, auto-detected from the bridge_spawn caller |
normal MCP spawn from the primary |
| 3 | The bridged daemon's own cwd |
REST spawn / off-host caller — never $HOME |
The primary's cwd (source 2) is discoverable with no new plumbing: bridged already resolves the MCP
caller's loopback peer PID for connection identity (ConnectionIdentity → LsofPeerPidLookup);
the same PID yields its cwd via lsof -a -p <pid> -d cwd -Fn (the n… line). The primary maps to no
worker pane (it is not a worker), but its PID and cwd are still readable.
sequenceDiagram
participant P as "Primary (main)"
participant B as "bridged"
participant O as "OS (lsof)"
participant H as "herdr"
P->>B: "bridge_spawn {profile} (no cwd)"
B->>O: "peer PID for this connection's port"
O-->>B: "pid"
B->>O: "cwd of pid (lsof -d cwd)"
O-->>B: "/Users/you/LTMS/claude-bridge"
B->>H: "tab.create (placement)"
B->>H: "agent.start {argv, env, tab_id, cwd}"
H-->>B: "worker in the primary's directory"
Figure 2 — a no-cwd spawn inherits the primary's directory from the caller's PID.
Status: implemented (CB-112).
bridgedthreads the resolvedcwdontoagent.start {cwd}(verified: the worker process is rooted there), keeping the single shared worker space. On an MCPbridge_spawnthe primary's cwd is auto-detected from the caller's PID; over REST (no MCP caller) it is the explicitcwdparam else the daemon's cwd. Both placements (taband legacypane) carry it, since it ridesagent.start.
Assumed launcher: ccs (Claude Code)
For now the fleet assumes ccs (Claude Code under the hood) as the worker CLI — argv: ["ccs", "<profile>"]. Its startup gate is the folder-trust dialog.
How ccs/Claude Code decides whether to prompt
Trust is recorded per-directory, per config dir. Each ccs profile is an isolated instance with
its own config dir (~/.ccs/instances/<profile>/) and its own .claude.json:
// ~/.ccs/instances/<profile>/.claude.json
"projects": {
"/Users/you/LTMS/claude-bridge": { "hasTrustDialogAccepted": true }, // trusted → no prompt
"/Users/you": { "hasTrustDialogAccepted": false } // untrusted → prompts
}
The worker prompts iff its cwd is not marked hasTrustDialogAccepted: true for that instance.
This is why the directory rule above matters: land workers in the primary's project folder and you
grant trust once per profile, instead of scattering trust across $HOME and ad-hoc dirs.
Clearing the prompt (ranked)
- Inherit the primary's project dir (the rule above) and trust that folder once per profile.
- Pre-trust interactively: run
ccs <profile>in the target folder and accept — persistshasTrustDialogAccepted: truefor that path in the instance's.claude.json. - Set the flag directly (scriptable, no interaction): set
projects["<cwd>"].hasTrustDialogAccepted = truein~/.ccs/instances/<profile>/.claude.json. - Do not reach for
--dangerously-skip-permissions— it disables all permission gating, not just this dialog, which defeats running off-subscription workers autonomously.
Other CLIs: different launchers, different prompts
ccs is the current assumption, not a hard dependency — a worker profile's argv can be any CLI.
Each launcher has its own first-run/trust gate, so the "clear the prompt" step is CLI-specific
and belongs with the profile, not hard-coded:
Launcher (argv) |
Startup gate | How to clear it |
|---|---|---|
ccs <profile> (Claude Code) |
Folder-trust dialog | hasTrustDialogAccepted per project in the instance's .claude.json (above) |
Other Claude-compatible runtimes via ccs (codex, gemini, cursor, …) |
Each has its own first-run / trust / login prompt | Per-runtime; document per launcher as it is adopted |
A bare command (bash -c …, mechanics probe) |
None | n/a — used for non-interactive smoke tests |
When adding a new launcher, capture its startup-prompt behaviour here (what blocks, and the non-interactive way to satisfy it) so a spawned worker of that kind reaches an injectable prompt unattended.
See also
docs/MCP-Contract.md— the tool surface (bridge_spawn,bridge_profiles, …).wiki/2-Message-Server.md— the herdragent.*/workspace.*schema (workspace.create {cwd}).