acpmux: a full-access chat's sub-session under its own trusted folder asks no trust

Merged
TE
12d ago
+623-5

Leo, 2026-10-24, word for word, about a "Needs your input" folder trust card that claude-sr2 (a chat in Full access, Claude Code bypassPermissions) raised when it started the sub-session update-toggle: "Also, like, why did this even happen in the first place? I thought we bypassed this."

Why the bypass didn't apply

  • The earlier fix (Leo 2026-10-21, "trust automatic for cmux") covers folders cmux made: agent-home folders, and worktrees start_session makes (trust.rs made_by_cmux, trust_wait.rs trust_made_worktree). It never looked at the starting chat's mode. Before this PR, Hub::ensure_folder_trusted raised a card for every folder no decision covered, whatever the parent's access.
  • A trust record covers only its own folder, or a repository's worktrees through via. Its subfolders are not covered. So even a start under the parent's own trusted folder raised a card.
  • In Leo's case the sub-session's cwd was /private/tmp/claude-sr2/lanes, a lane scratch folder. claude-sr2 made it with a Bash mkdir -p (its acpmux log, seq ~382398 and earlier). claude-sr2's own cwd is /private/tmp/claude-501/-Users-leoli-Projects/<uuid>/scratchpad, which Leo trusted. lanes is neither under that folder nor a folder cmux made, so nothing answered for it.

What changes (scope: Full-access parent, the parent's own folder)

trust_wait.rs inherits_parent_trust: a start skips the card only when all of these hold:

  1. the starting chat runs in full access by cmux's own doing (full_access_by_cmux, added after Codex review): its current mode is a full-access mode cmux itself set. A launch command that skips the checks doesn't count, because it stays in the record after a fallback to another profile (fifth review round). That's the new SessionMeta.mode_set_by_cmux, written where acpmux asks the harness for a mode (forward's session/set_mode, which covers the start default and the person's person-gated pick; the policy mode; the full-access cap). Each new agent process starts without it (fresh_agent, eighth review round): a failover or respawn gets the mark back only once cmux puts that agent in the same mode again (a successful replay session/set_mode, or the --permission-mode cmux starts Claude with when the profile pins none). A fallback that reports full access by itself, or refuses the replay, gets nothing. An imported bundle has it cleared. A mode the harness reports on its own (current_mode_update) never counts, because the agent controls that report: a reported mode other than the mark clears the mark, so a harness that leaves cmux's mode and comes back gets nothing (sixth review round). A child's mode that is granted only because it matches its caller's mode (StartGrant::Inherit), or granted for a parent's full access in settle_subsession, keeps the mark only when the parent's own mode is cmux-set. So a claimed mode can't be passed down to a child either (third review round);
  2. it is not a Web turn, and the chat is not one a Web client started (remote_origin; added after Codex review);
  3. the child's resolved cwd is the parent's cwd or under it (Path::starts_with, after resolved_folder, so .. and symlinks can't escape), and the parent's recorded cwd already resolves to itself. A parent folder named through a link, which the agent could retarget after it started, lends nothing (fourth review round);
  4. a decision trusts the parent's folder for the parent's agent: a person's answer, the agent's own level, or a folder cmux made. An auto-trust rule alone does not count;
  5. nobody marked the child's folder untrusted. The folder walk after it (subsession_folder_check) still refuses any folder marked untrusted up to /.

Nothing new is recorded: the child's folder gets no trust record, and ensure_folder_trusted returns "no decision", so a worktree made from it is not recorded as trusted either. Chats that ask, Web turns, folders outside the parent's folder, and parents whose folder nobody decided all keep the card exactly as before.

Not done, by the brief's stop rule: the brief also named "a folder the parent created". acpmux has no reliable signal for that. claude-sr2 created lanes with a Bash command, which reaches acpmux only as free-form shell text, and a birth-time check can't tell which process made a folder. Guessing from either would trust paths the parent didn't verifiably create, so this PR leaves it out. As a result, Leo's exact case (/private/tmp/claude-sr2/lanes, outside claude-sr2's cwd) still shows the card. A start under claude-sr2's own folder no longer does. Making Leo's case card-free means either running lanes under the coordinator's own folder, or a trust policy decision for Leo, which I haven't made.

Test

New cmux-tui/crates/acpmux/tests/subsession_trust_inherit.rs, run on huge-blue:

  • Red on base (feat-cmux-next cb1135a6462 plus the test only): fails at "a card under its own folder", because the Full-access parent's start under its trusted folder raised a card.
  • Green on head:
    • a Full-access parent in a trusted folder starts full/lanes/update-toggle and its own folder with no card, and no record is written for lanes;
    • the same parent outside its folder still gets a card;
    • a person-untrusted folder under it is refused (subsessions.folder_untrusted);
    • an Ask-mode parent (currentModeId: ask) in a trusted folder still gets a card under its folder;
    • a Full-access parent whose folder nobody decided still gets a card;
    • a chat that asks, whose harness then reports full-access on its own (fake agent set-mode:), still gets a card. This is red with the old caller_access gate;
    • a same-family child of that chat, started in its trusted folder (it inherits the claimed mode), still gets a card for its own start under it. This is red without the inherit fix;
    • a chat cmux set to full access whose harness leaves the mode and reports it again still gets a card. This is red without the fix;
    • a chat launched with --dangerously-skip-permissions but in no mode cmux set still gets a card. This is red without the fix;
    • a Full-access chat whose agent died and started again still lends its trust (cmux replayed the mode);
    • a Full-access chat that failed over to a profile which starts in full access by itself and refuses set_mode still gets a card. This is red without the fix;
    • a Full-access chat whose folder was named through a symlink still gets a card under the link's target. This is red without the fix;
    • a chat a Web client started, in a trusted folder and outside a Web turn, still gets a card. A Web chat in full access needs the person's own mode switch in the app, which the hermetic test can't make, so the remote_origin guard has no direct red case.

After the trust read, the parent is read again: if it left cmux-set full access, or changed folder or harness, during the read, it lends nothing. settle_subsession keeps a child's mark only when the parent's mode is cmux-set both at the start and after the child's mode was applied (ninth review round). A hermetic test can't change the parent's mode in the middle of that await, so this guard has no red case.

settle_subsession also grants the child its start mode from the parent's mode as it is now, not from the snapshot taken when the start was checked. A parent that switched to Ask in that window can't hand a cross-family child full access (tenth review round).

A parent recorded at a path that isn't its resolved path lends trust only when every link on the way, and in the links' targets, is root's, in a folder root owns that others can't write (only_root_links, tenth review round). So /tmp/x on macOS (/tmp → private/tmp) works, and a link the person owns still lends nothing. Unit tests in trust_wait.rs: a person-owned link is refused, and on macOS /tmp is accepted.

The folder walk after the card also reads the parent as it is then, not the start-time snapshot (eleventh review round). The child is also capped to the parent as it is after the child's mode is set (hold_subsession, twelfth review round). So the parent's mode is read live at every step: the inherit check, the walk, the child's mode, and the cap. A start that has passed all of them when the parent switches goes on, the same as a start already past its checks when a trust record changes.

Two later review points are deliberate, not changed:

  • A command-line bypass doesn't count, for example a profile that pins --permission-mode bypassPermissions or passes a skip flag. Only a mode cmux set counts, because a launch command stays in the record after a fallback (fifth round). A person's Full-access chat sets its mode through session/new's start mode (start_in_mode → set_mode), so it is marked.
  • A parent's folder renamed and replaced at the same path: trust is recorded per path, as everywhere else in cmux. A Full-access parent can already write anything into its trusted folder, so swapping the folder gives it nothing it didn't have.

Codex also raised a read-then-walk race: the trust is revoked between this read and the start. The existing card path has the same window (trusted_now, then the walk), and a start that began a moment before the revocation would have gone on anyway, so this PR doesn't change it.

  • tests/subsession_trust.rs (1/1) and tests/subsessions_safety.rs (17/17) are unchanged and pass. In the full cargo test -p acpmux --no-fail-fast, the only failures are the same on clean base 4097397 (protocol_registry_names_every_served_method, daemon_chat_open, remote_guard_codex_bridge Ɨ2, session_list_order) or are load flakes that pass when run alone (transfer_guard::* 12/12, watch::*). cargo clippy -p acpmux --all-targets -- -D warnings is clean. cargo clippy -p acpmux --all-targets reports nothing in the changed files, and rustfmt --check is clean.

Risk: this is trust policy. It widens trust only for a Full-access chat, which already runs any tool without asking, and only to folders under its own decided-trusted folder.

šŸ¤– Generated with Claude Code

Agent session: cosmic-gray