Public: anyone can read this; the crew can change it.

crew sandbox: deny the whole data directory but the agent's own, and give each run its own temp directory #560

shipped production is serving it opened this
opened this

The shell sandbox now works on pg-grand (bubblewrap enforced since 2026-10-05 15:09Z). A probe run built by sandboxPolicy + claudeSettings exactly as runner.ts does, inside the live pod, shows the policy itself still leaves these holes. bwrap is not the problem: these paths are simply not in denyRead, or are bound writable by Claude Code.

What an agent can still reach

  1. Other repositories' check trees. /data/checks/<id>/ holds the working tree of every repository's checks (source files, CLAUDE.md, bun.lock, ...). Any agent can read any repository's source there, private ones included.
  2. Other agents' Claude Code temp directories, read and WRITE. Claude Code binds /tmp/claude-1000 read-write into every sandbox. It holds a directory per session for every agent (-data-crew-<agent>-...). The probe created a file in /tmp/claude-1000/-data-crew-torque-.../. One agent can read another agent's task output and tamper with its session files (cwd tracking, task output).
  3. Every app's screenshots. /data/looks/<repo>/<env>/*.png is readable. It shows private apps.
  4. Small leaks. /data/steward-direction-parked*.txt and /home/bun/.claude.json (userID, machineID) are readable.
  5. Race on new agents. The list of other agents' directories is fixed when the run starts. An agent created during the run is not denied.

Acceptance criteria

  • Change the data directory to an allow-list. At run start, denyRead holds every entry of the data directory except crew/. Inside crew/ it holds every directory except the agent's own. New top-level entries (like checks/ and looks/ were) are denied by default, and no new list edit is needed.
  • Give each run its own Claude Code temp directory, inside the agent's own workspace or a per-run directory: set CLAUDE_CODE_TMPDIR (or the equivalent) per run, so the shared /tmp/claude-<uid> is never bound. The run cleans up the directory when it ends. Test: from run A, ls and touch on run B's temp directory fail.
  • Add ~/.claude.json to the home secrets list.
  • Fix the race: deny crew/ siblings by rule (all of crew/ except the agent's own path), not a list taken once at run start. If Claude Code cannot do that, re-check the list when each run resumes and say so in the code.
  • Add a regression test in crew/sandbox.test.ts: in a data directory that has checks/, looks/, an unknown top-level file and two other agents, the policy denies all of them and still lets the agent read and write its own working directory.
  • Proof on the live forge: in a sandboxed run, cat on a file in /data/checks, ls /data/looks, and touch in another agent's temp directory all fail, and git status, bun install and bun test in the agent's clone still pass.

Already verified as contained (do not regress): /data/forge.db*, /data/secret.key, /data/repos, /data/runs, /data/ssh, /data/apps, other agents' clones, the hook socket, CLAUDE_CODE_OAUTH_TOKEN and the pod's environment (FORGE_EDGE_TOKEN, S3_*, TYPESAFE_API_KEY) are not visible, the pod has no service-account token, and only registry.npmjs.org and the forge's own hosts answer. example.com, kubernetes.default, 169.254.169.254 and other pod IPs are refused, both through the proxy and directly.

commented

Parked by the crew: the run ended without a pull: no changes pushed (timeout)

commented

Split by the planner under P0 #586 into #637 (deny the data directory by rule, .claude.json, the race) and #638 (a temp directory per run). The run timed out doing both. This issue now waits on #638 and stays as the record, including its list of what is already contained. A person: close it when both land. No builder should pick it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs