Skip to content

Workspace sandbox (macOS only)

A workspaced tab (agent -w / harness -w) confines its processes — the tab's shell, harness PTY, or ACP session, and anything they spawn — to the workspace directory using a kernel-enforced Seatbelt sandbox (sandbox-exec):

  • Writes are denied everywhere except the workspace and its private temp dir, plus a narrow set of harness-state carve-outs (~/.claude/projects, ~/.claude/session-env, ~/.claude.json, ~/.codex, ~/.config/opencode, and a few package-manager caches) and two fixed OS-level paths a harness needs regardless of $HOME: the real per-user Darwin cache directory (a legacy Keychain subsystem locks a file there) and the harness CLI's own scratchpad directory (/private/tmp/claude-<uid>/, created before every tool call — without this carve-in, no Bash tool call works inside a workspaced tab at all). Never the whole ~/.claude, ~/.cache, or ~/.npm — broad cache write access would let a sandboxed agent poison packages a non-sandboxed process later consumes.

  • Reads of $HOME's contents are denied by default and carved back in only for the write carve-outs above, plus ~/.gitconfig, ~/.config/gh/config.yml (gh's general settings — not hosts.yml, which stays denied), ~/.claude/settings.json, and ~/Library/Keychains (needed so a harness can look up its own OAuth credential — see Known limitations below). System paths (/usr, language runtimes, Homebrew) stay readable, as does a harness's own executable directory even when it lives under $HOME (nvm, ~/.opencode/bin, …).

  • A fixed list of secret paths is denied even inside a carve-in: .ssh, .aws, .gnupg, .kube, .netrc, cloud CLI configs (gh hosts.yml, gcloud, azure, docker), credential files for Cargo/PyPI/Maven/Terraform, shell/REPL history files, and browser profile directories (Chrome, Firefox, Brave, Safari).

  • Environment variables that could bypass the file-read denies above are stripped before spawn: AWS_*, GITHUB_TOKEN, GH_TOKEN, NPM_TOKEN, DOCKER_*, KUBECONFIG, anything ending _SECRET/_PASSWORD, SSH_AUTH_SOCK, GPG_AGENT_INFO, GNUPGHOME, GIT_ASKPASS, GIT_CREDENTIAL_HELPER, KRB5CCNAME. LLM provider keys (ANTHROPIC_*, OPENAI_*, GEMINI_*/GOOGLE_*) are deliberately kept — the harnesses need their own credentials to function. A JANISSARY_NODE variable is added, set to the janissary server's own Node binary path — so a script inside the sandbox (e.g. a project's own .claude/settings.json hook) can invoke a known-good node without depending on PATH resolution inside the sandboxed context. If a scoped GitHub token is configured (see "GitHub push/PR access" below), GH_TOKEN is re-added after the strip, set to that token — the one deliberate exception, since it's a fresh value chosen for that workspace, not the ambient one just removed — and GH_CONFIG_DIR is pointed at an empty, workspace-private directory, since gh reads ~/.config/gh/hosts.yml on every invocation regardless of GH_TOKEN and treats the sandbox's deny on that file as a fatal error rather than falling back; redirecting GH_CONFIG_DIR gives it a genuinely absent hosts.yml instead.

  • Network is allowed by default; add --offline to a workspaced tab to deny it instead:

    harness claude -w --offline    → workspaced harness tab with no network access

Practical consequences. No global installs, no reading sibling workspaces/other repos/dotfiles outside the carve-ins above. git commit/fetch/pull, npm install, builds, venvs, and harness login all work normally inside the workspace. The workspace's origin is HTTPS, pointed directly at GitHub — git push and gh (PR create/merge) work from inside the sandbox too, if a scoped GitHub token is configured (below); without one they fail, since .ssh is denied and SSH_AUTH_SOCK is stripped.

GitHub push/PR access

To let workspaced tabs git push and use gh (PR create/merge), create a fine-grained personal access token scoped to just this repo, with Contents: Read and write, Pull requests: Read and write, and Metadata: Read-only permissions — nothing broader. Save it to .janissary/github-token (already gitignored; janissary only reads this file, never writes it). Without this file, workspaces still work for local development, just not pushing/PR operations.

Configuration

Isolation is on by default; set "sandboxWorkspaces": false in .janissary/config.json to disable it (e.g. on a non-macOS host, or if it interferes with a particular harness). It also requires sandbox-exec to be present, which rules out non-macOS hosts. When a workspaced tab is created and isolation isn't actually active — the config key is off, or sandbox-exec is unavailable — a one-line notice is appended to the tab's transcript.

Known limitations

  • ~/Library/Keychains is readable, unlike the other secret paths above. Even "modern" Keychain Services calls fall through to a legacy implementation on macOS that reads the keychain database file directly rather than only talking to securityd over IPC — denying that read blocks every Keychain lookup a sandboxed process makes, including a harness's own OAuth credential, and it fails silently (the harness just reports "not logged in", with no permission error to explain why). The database itself stays encrypted and per-item ACL-enforced by securityd regardless of raw file readability, so this doesn't hand out plaintext secrets — but it is a materially larger read surface than the rest of the sandbox's design intends, kept as a deliberate trade-off.
  • The sandbox confines filesystem and network access; it does not sandbox CPU, memory, or other system resources, and a sandboxed process can still make outbound network requests (unless --offline is set).
  • macOS only — on other platforms, -w still creates the disposable git workspace, just without process confinement (a transcript notice says so).