fix(security): wire the permission bridge, sandbox, and worktree preservation into production - #3
Conversation
…preservation into production Three public claims did not match the code (ADR-034): - Approvals said they "hold until you approve". `onPermissionRequest` was only ever called by `mock-runner.ts`, so in production they never fired. The Claude CLI engine now raises every gated tool call through a loopback HTTP MCP bridge and holds for the run's remaining wall-clock budget. `--permission-prompt-tool` is present in CLI 2.1.261, which falsifies ADR-020's premise; the contract matrix was re-run to prove it. - Docs said every run executes inside a macOS Seatbelt sandbox. The generator, symlink resolution, credential-collision refusal and the 13 escape checks were all real, but no production spawn passed a spec. Every engine spawn now routes through `applySandbox()`. `CW_SANDBOX=off` is the only way out and it is logged, journaled, and stamped on the report. - Docs said an interrupted run keeps its worktree. S-39 removed it whenever the agent had committed nothing, including on timeout. Pruning is now limited to runs that ended cleanly with no git operation in flight. Under `acceptEdits` the CLI runs Bash without consulting the prompt tool, so the deny-list floor is enforced by a PreToolUse hook, which fires in every permission mode. Also makes the cancel test deterministic. The daemon marks a run 'running' at spawn, before the child has run a line, so cancelling on that signal raced the child's first message and `sandboxed` was legitimately null. The test now waits for the containment stamp, so it interrupts a run that is genuinely under way. Test-only change; `sandboxedFor` semantics are unchanged. Suite: 260 passed, 37 files. Lint and typecheck clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The PreToolUse floor hook exited right after writing its deny reason to stderr. On macOS a pipe-backed stderr write is asynchronous, so the reason the model reads could be truncated (the exit code, and therefore the deny, was never at risk). The hook now exits from the write callback with a ref'd 1s fallback that still guarantees exit(2). Docs catch up with what ships: docs/security.md closes the "policy floor only sees what the CLI asks about" gap with the 2026-09-06 production probe (npm view still prompts; git push --force is refused with the deny-list reason; ~60 ms per Bash call), README gains the floor bullet, the T-007 matrix gets the "floor closed" row, and ADR-035 records three decisions: the fail-closed hook, exactly one Seatbelt layer for Codex (macOS refuses to nest a profile inside a deny-default one), and TERMINAL_CWD for Hermes whose oneshot path ignores --in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addendum, Floor proven end to end through the production
In-sandbox engine probes (production Release worktree at this head: typecheck 0 · runner 116/116 · daemon 127/127 · UI build 0. Surfaced, not decided: LICENSE §12's audit set should list |
…ff the environment
Two findings from the pre-release review, both reproduced on this Mac.
A repo committing {"disableAllHooks": true} in .claude/settings.json switched
the PreToolUse floor hook off, so under acceptEdits a floor-listed command
ran unasked while the report still said sandboxed: true. The --settings
payload now pins disableAllHooks: false; CLI-flag settings outrank project
settings, so the pin wins. Probed on CLI 2.1.261: unpinned payload, hook
never ran and the command ran; pinned payload, hook ran and the command was
blocked. Unit test pins the key.
The BYOK provider key travelled in runner-child's environment and was
deleted after being read. macOS keeps a process's exec-time env readable by
any same-user process through sysctl KERN_PROCARGS2, and the profile must
allow sysctl-read, so a sandboxed agent read the key from a sibling child
after the delete. The daemon now writes {t:'credential'} to the child's
stdin pipe right after spawn, only for BYOK runs; the env never contains it.
The child races the message against a 15s timeout and fails with the same
auth text either way (verified against the built child: 0.1s with an empty
message, 15.1s with none).
Also from review: codex/opencode/hermes/api-agent runners now remove their
per-run cw-sb-* profile dir on close, error and spawn failure (the api agent
leaked one per tool call), and the three CLI runners gain the child 'error'
handler they lacked. docs/security.md names the remaining gaps (Bash-only
hook matcher vs repo MCP servers, no bearer token on the loopback bridge);
ADR-035 records both findings.
Runner 124/124, daemon 130/130, typecheck and build clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…YOK env claim Review follow-ups on the previous two commits. The generated PreToolUse hook's watchdog now marks the outcome settled before denying, so a bridge reply landing in the one-second window after a watchdog deny can no longer turn into exit(0). docs/security.md qualifies the BYOK statement: a config that reads its key from an environment variable has that key in the daemon's own exec-time environment by construction, which the same sysctl path can read; keychain-backed configs are fully closed. Four comments and test titles that cited ADR-034 for the credential channel and the profile cleanup now point at ADR-035, where those findings are recorded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review outcome and fixes (
Also from review: Reviewer's own re-run on this checkout: 39 files, 271/271. Mine: typecheck 0, build 0, runner 124/124, daemon 130/130; production floor E2E passed again on the final dist. |
Three statements the project makes in public did not match the code. Each gap was a missing call site in front of working, tested code. ADR-034 records the decision; this PR is the wiring plus guards that read the sources, so the call sites cannot silently disappear again.
What was wrong
onPermissionRequestwas called only bymock-runner.ts, so no production run ever raised one.What changed
--permission-prompt-toolis present in CLI 2.1.261, which falsifies ADR-020's premise. The contract matrix was re-run against the installed version to prove it.applySandbox().CW_SANDBOX=offis the only way out, and it is logged, written to the safety journal, and stamped on the report.acceptEditsthe CLI runs Bash without consulting the prompt tool, so the deny-list floor moved to aPreToolUsehook, which the CLI invokes in every permission mode.Testing
Suite green: 260 passed across 37 files. Lint and typecheck clean.
The cancel test was also made deterministic. The daemon marks a run
runningat spawn, before the child has executed a line, so cancelling on that signal alone raced the child's first message andsandboxedwas legitimately null. The test now waits for the containment stamp before interrupting. That is a test-only change;sandboxedForsemantics are untouched.Note for the reviewer
A stacked PR with the agent-workforce features is based on this branch. Merge this one first, then that one retargets to
main.🤖 Generated with Claude Code