An agent’s authority freezes at the cwd it started from, not the cwd it stands in
Exarch freezes an agent’s authority against the cwd it was started at — the
trunk against its launch cwd, a desk child against its session’s live cwd at
the instant it was spawned — and never re-freezes on a cd. One captured cwd
fans out to the capability ceiling, AGENTS.md/skills discovery, project
identity and logs, and /export’s anchor; the live shell seeded at that same
place drifts with every cd the model issues, but the grants that watch over
it do not follow. Realised in exarch/src/lib.rs (run), exarch/src/policy.rs
(for_invocation, narrow), exarch/src/agent/seat.rs (boot_root_shell,
Seat::identity), exarch/src/fleet/desk.rs (agent-start), exarch/src/prompt.rs
(assemble), exarch/src/bootstrap.rs (project_dir, log_run_dir), and
exarch/src/tui/commands.rs (resolve_export_path).
Context
A trunk reads its cwd exactly once, at launch: run() calls
std::env::current_dir(), and the resulting string is threaded — not
re-derived — into everything that follows in the same invocation:
policy::for_invocation’s FreezeCtx (the ceiling ~/xdg:/cwd: sigils
resolve against), bootstrap::EXARCH.project_dir/log_run_dir (the state and
log directories a project’s runs share), prompt::assemble’s AGENTS.md and
skill discovery, and SessionInfo.cwd, which tui::commands::resolve_export_path
anchors a relative /export path against — its own doc comment says the
reason plainly: “anchor a still-relative path at the launch cwd rather than
the process’s own.” The live session shell is seeded at that identical spot
(seat::boot_root_shell calls shell.seed_cwd(cwd)), and from there it is
free to drift: a model’s cd moves the shell’s own notion of “here” for
every relative path it types next, and nothing in this list re-reads it.
A desk-spawned child is the same shape from the child’s side. HostServices
is rebuilt fresh at every ral call from the parent’s live directory —
Agent::cwd() probes the running shell rather than reading the parent seat’s
stored cwd, “so a desk-spawned child starts where the model is.” agent-start
narrows policy::narrow(&s.caps, spec.grant, &s.cwd) against exactly that
probed value, then Seat::identity(shell, scratch, s.cwd.clone(), …) seeds the
new child’s shell there and stores the same value as the new seat’s own cwd
field — the one /clear rebuilds from verbatim, never by probing again.
Decision
cdmoves where relative paths resolve; it does not move the anchor grants are frozen against.cwd:in a profile “resolves to where the grant was established, so a latercdcannot retroactively widen authority — the body cannot widen by moving” (capability-freeze). This page names the exarch-level instance of that rule: the anchor is not the live shell’s cwd at enforcement time, it is the cwd in force when the agent came into being.- The trunk’s anchor is the launch cwd, captured once by
run()and passed by value to every consumer above. Nothing downstream re-derives the anchor: the two process-cwd reads that remain — the TUI title bar’s basename and the prompt’s host-factscwdline — are display-only, and exarch never chdirs its own process, so they can only restate the same value. - A desk child’s anchor is its session’s live cwd, taken at the moment it is
started — the parent’s cwd as
agent-startsees it when the spawn runs, which is where the child’s own existence begins.Seat::identityfixes that reading into the child’s own seat once, andSeat::clear(/clear’s engine half) rebuilds from the same stored field rather than probing the live shell again, so the child’s own anchor does not drift either. - One rule, read from both ends: grants freeze where the agent was
started. A trunk’s “started” is the process’s launch; a child’s is the
agent-startcall that gave it its own identity.
Consequences
- Re-freezing on
cdwould silently re-anchor authority nobody re-granted — an agent that wanders outside its ceiling gains nothing by standing there, and one that wanders back out of a directory it was allowed keeps what it was given, exactly ascapability-freeze’scwd:sigil already guarantees one level down. - A launch cwd that cannot be read is not silently absolutized to
.; the launch refuses outright, because everything downstream — the ceiling, the project’s log directory,/export’s anchor — would otherwise freeze against a fabricated “here” nobody chose. - Project identity and logs stay keyed to the directory the session was
launched from for its whole lifetime:
project_dir/log_run_dirare computed once, so a model’scdcannot fragment one project’s state or logs across two directories mid-session. /exportkeeps resolving a relative path against the launch cwd even after the model hascd’d elsewhere, matching what a user watching the launch directory expects to find.
See also
capability-freeze, grant, one-walk-one-anchor.
Cite: exarch/src/lib.rs (run), exarch/src/policy.rs (for_invocation,
narrow), exarch/src/agent/seat.rs (boot_root_shell, Seat::identity,
Seat::clear), exarch/src/fleet/desk.rs (HostServices::cwd, agent-start,
ExarchDesk::launch), exarch/src/agent.rs (Agent::cwd), exarch/src/prompt.rs
(assemble), exarch/src/bootstrap.rs (App::project_dir, App::log_run_dir),
exarch/src/tui/commands.rs (resolve_export_path, cmd_export).
Read against f414ea84.