Run the plan's folder commands in a macOS sandbox with no network and no writes

Merged
CH
16d ago
+142-9

Summary

  • The problem. Since #1777, the service hands the Mac a bash command for each run_in_repository call, and the Mac ran it as the developer with full permissions. Anyone who controls the service, or steers the planning model, could run any command on a tester's Mac.
  • The fix. On macOS each command now runs under /usr/bin/sandbox-exec with a deny-by-default profile (packages/host/src/planning-commands.ts):
    • no network;
    • no writes, except /dev/null, /dev/zero, and /dev/fd;
    • reads only in the plan's folder (passed as a parameter, with links resolved) and system paths (/usr, /bin, /System, Xcode tools, /opt/homebrew, /etc);
    • no system service lookups except the user lookup, so a command cannot ask a service such as open to act outside the sandbox;
    • no reading a file named .env or starting with .env (.env.local, .envrc), even in the folder. The deny is the profile's last rule, so it beats the folder's allow.
  • The profile follows the shape of Codex's read-only Seatbelt policy. Claude Code's sandbox-runtime and Chrome use the same mechanism. Apple marks sandbox-exec deprecated, but it is still the only documented way to sandbox an arbitrary process outside the App Store.
  • Environment. Every command gets a minimal environment of its own, so env cannot send Luke's variables to the service. bash -lc is now bash -c, since a login shell would read dotfiles outside the folder.
  • The tool description tells the planning model it can only read and has no network. PRIVACY.md describes the sandbox.

Known gaps (fine for the friends MVP)

  • Anything else inside the folder can still be read and sent to the service, such as a key committed in a config file.
  • File metadata is readable everywhere, so a command can learn that a file outside the folder exists, but not read it.
  • Off macOS, commands run without a sandbox. Luke ships only for macOS, so this only affects tests on Linux.

Evidence

  • ./scripts/check.sh: exit 0.
  • New tests:
    • a command sees none of Luke's environment (runs everywhere, fails without the change);
    • on macOS, a command can neither write, nor read outside the folder or a .env.local inside it (runIf(darwin), so it is skipped on Linux CI).
  • CI could not verify this, and it has not run on a Mac. The work was done on Linux. Before it lands, run on a Mac: pnpm exec vitest run packages/host/src/compose-planning.test.ts, then a git log in a real plan (/usr/bin/git goes through xcrun, which is the most likely thing to break in the sandbox), or ./scripts/verify.sh.

šŸ¤– Generated with Claude Code


Open workspace in Conductor