Execution Environment
Agent system authors — where does the agent actually run? Which machine, which directory, which sandbox container? Claude Code's 3 isolation modes (none / worktree / remote) + cross-platform process sandbox + CCR cloud architecture.
Distinct from “permissions”
The previous chapter Permissions covered what can be done — the trust layer of modes / rules / hooks.
This chapter covers where it runs — which machine, directory, sandbox container the agent process lives in. The two questions are orthogonal: one command might be permission-allowed without a sandbox (default CLI mode), or run in a sandbox but rejected by a hook.
The “sandbox” in Zapvol’s vocabulary (backend/infra/sandbox/ — Node / Daytona / E2B) corresponds to this
chapter, not the previous one. Two systems for different problems — this chapter keeps them clearly separated.
Three isolation modes
Claude Code exposes 3 isolation levels via Agent tool (source tools/AgentTool/AgentTool.tsx line 99):
isolation: z.enum(['worktree', 'remote']).optional()
// omitted = default mode (runs in user's current cwd)
// 'worktree' = temporary git worktree isolation
// 'remote' = CCR (Claude Code Remote) cloud execution
| Mode | Filesystem | Network | Process | Blast radius |
|---|---|---|---|---|
| Default (no isolation) | User’s cwd | User’s network | User’s machine | Shares fate with CLI itself |
| worktree | Temp git worktree (.claude/worktrees/<slug>) | User’s network | User’s machine | Isolates file changes, not process |
| remote | CCR cloud environment | CCR network | Separate Linux container | Fully isolated |
Key insight: the three modes represent progressively stronger isolation from easy to hard — but cost increases too. Default is fastest with highest risk; remote is safest but requires several seconds to spin up cloud environment + GitHub app setup.
Task.ts’s 7 TaskType values are orthogonal to this: local_agent runs locally (default or worktree mode),
remote_agent runs in CCR, in_process_teammate runs in the same process. TaskType describes what kind of
task, isolation describes where the task runs.
Worktree isolation: ephemeral git twin
Creation
Source utils/worktree.ts line 902, createAgentWorktree:
export async function createAgentWorktree(slug: string): Promise<{
worktreePath: string
worktreeBranch?: string
headCommit?: string
gitRoot?: string
hookBased?: boolean
}> {
validateWorktreeSlug(slug)
// 1. First: hook-based creation (for non-git VCS like mercurial / sapling)
if (hasWorktreeCreateHook()) {
const hookResult = await executeWorktreeCreateHook(slug)
return { worktreePath: hookResult.worktreePath, hookBased: true }
}
// 2. Fallback: native git worktree
const gitRoot = findCanonicalGitRoot(getCwd())
if (!gitRoot) {
throw new Error('Cannot create agent worktree: not in a git repository...')
}
const { worktreePath, worktreeBranch, headCommit, existed } =
await getOrCreateWorktree(gitRoot, slug)
// ...
}
Source-level details:
findCanonicalGitRoot — avoid worktree-inside-worktree
Comment (lines 922-925):
findCanonicalGitRoot (not findGitRoot) so agent worktrees always land in the main repo’s
.claude/worktrees/even when spawned from inside a session worktree — otherwise they nest at<worktree>/.claude/worktrees/and the periodic cleanup (which scans the canonical root) never finds them.
Meaning: if the current process is already inside a session worktree and creates a new subagent worktree, it must go back to the main repo root — otherwise nested worktrees, and the periodic cleanup can’t find them.
Another real production bug fix, not theory.
Slug validation against path traversal
Lines 48-49:
const VALID_WORKTREE_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/
const MAX_WORKTREE_SLUG_LENGTH = 64
validateWorktreeSlug rejects:
../../target(parent directory escape)/absolute/path(absolute path)./..segments alone- Over 64 chars
The comment explains the strictness: join('.claude/worktrees/', slug) after path normalization, .. can
escape the worktrees directory.
Takeaway for your own agent: any user-controllable string destined to become a file path must use strict allowlist validation — not escape / sanitize, but accept only explicitly-safe character classes.
Hook-based fallback: support non-git VCS
When hasWorktreeCreateHook() returns true, a hook path is taken — letting users plug in any VCS
(mercurial / sapling / perforce) via WorktreeCreate / WorktreeRemove hooks. The returned path is treated as
“an isolation directory shaped like a worktree.”
This is protocol-style extensibility: Claude Code doesn’t deeply bind to git; it defines an interface for “what a worktree should do” and lets users fill in specifics.
Bwrap ghost dotfile cleanup
utils/Shell.ts line 386:
On Linux, bwrap creates 0-byte mount-point files on the host to deny access to paths inside the sandbox; when bwrap exits as ghost dotfiles in cwd. Cleanup is synchronous and a no-op on non-Linux.
On Linux, bwrap creates 0-byte “mount-point” files in cwd to deny access — after bwrap exits, these become “ghost dotfiles” left behind. Claude Code wrote dedicated cleanup code.
The level of detail shows that production “sandbox execution” is nowhere near a simple exec() — each
sandbox type has its own leaks / cleanup / edge cases.
Periodic cleanup
Worktrees have periodic cleanup: scan .claude/worktrees/ for worktrees untouched for 30 days and delete.
Implementation has a reuse path:
Bump mtime so the periodic stale-worktree cleanup doesn’t consider this worktree stale — the fast-resume path is read-only and leaves the original creation-time mtime intact, which can be past the 30-day cutoff.
Fast resume is read-only and doesn’t update mtime — without explicit bumping, resuming an old worktree would cause next cleanup to delete it.
Remote mode: CCR (Claude Code Remote)
Remote isolation runs on a completely different architecture — Claude Code Remote (CCR), ant-only feature.
6 preconditions
tasks/RemoteAgentTask/RemoteAgentTask.tsx’s checkRemoteAgentEligibility + formatPreconditionError defines
6 reasons CCR can’t be used:
| Reason | User message |
|---|---|
not_logged_in | ”Please run /login and sign in with your Claude.ai account (not Console).” |
no_remote_environment | ”No cloud environment available. Set one up at https://claude.ai/code/onboarding?magic=env-setup” |
not_in_git_repo | ”Background tasks require a git repository. Initialize git…” |
no_git_remote | ”Background tasks require a GitHub remote.” |
github_app_not_installed | ”The Claude GitHub app must be installed on this repository first.” |
policy_blocked | ”Remote sessions are disabled by your organization’s policy. Contact your organization admin.” |
Inference: CCR’s architectural dependencies are GitHub code pulling + Claude.ai account identity + enterprise policy gating — not standalone container spin-up. SaaS integration + Anthropic-side runtime, not pure technical isolation.
Launching a remote agent
// AgentTool.tsx lines 433-457
if (effectiveIsolation === 'remote') {
const eligibility = await checkRemoteAgentEligibility()
if (!eligibility.eligible) {
const reasons = eligibility.errors.map(formatPreconditionError).join('\n')
throw new Error(`Cannot launch remote agent:\n${reasons}`)
}
const { sessionId, taskId, ... } = registerRemoteAgentTask({
remoteTaskType: 'remote-agent',
// ...
})
// ...
}
After launch, users can fetch a URL via getRemoteTaskSessionUrl(sessionId) to watch the agent’s progress (on
claude.ai).
Remote’s three key attributes
- Always
run_in_background: true— CCR is async; main agent doesn’t wait - Independent session — remote agents have their own sessionId, separate from main agent’s transcript
- WebSocket callback —
remote/SessionsWebSocket.ts+remote/RemoteSessionManager.ts— main agent receives subagent progress and results via WS
This is not exec-and-wait; it’s message-driven distributed agent architecture.
Bash’s process-level sandbox
Orthogonal to isolation: Bash tool’s own process isolation. Relies on external NPM package
@anthropic-ai/sandbox-runtime.
macOS: Seatbelt (sandbox-exec)
- Apple’s built-in sandbox-exec (Seatbelt)
- Write limited to cwd and /tmp
- Network denied by default; explicit opt-in per-host
- Zero install overhead, macOS ships with it
Linux: bwrap + seccomp + socat
Three separate dependencies (source SandboxDependenciesTab.tsx):
apt install bubblewrap # bwrap: filesystem isolation (not Docker, lighter)
apt install socat # socat: network proxy for per-host allow/deny
npm install -g @anthropic-ai/sandbox-runtime # provides seccomp BPF filter
seccomp BPF is critical — without it, there’s a hole. The source’s diagnostic explicitly says:
seccomp filter: not installed (required to block unix domain sockets)
Without seccomp, attackers can bypass bwrap’s network restrictions via unix domain sockets.
Windows: no sandbox
tools/PowerShellTool/PowerShellTool.tsx line 208:
On Windows native, sandbox is unavailable (bwrap/sandbox-exec are POSIX-only)
Windows Claude Code has no process-level sandbox. Permission system + deny-list + hooks are the only protection.
This is a real deployment-axis decision:
- macOS dev: kernel sandbox, can confidently enable permissive modes
- Linux dev: has sandbox after installing 3 deps; bare without them
- Windows dev: must rely on permission strategy — sandbox can’t catch things
The source has full SandboxDoctorSection / SandboxDependenciesTab UI components teaching users to install deps — meaning Anthropic is making sandbox diagnostics and onboarding a product-grade feature, not just an error message for devs.
@anthropic-ai/sandbox-runtime is a standalone NPM package
This is a separate NPM package; claude-code/utils/sandbox/sandbox-adapter.ts is only an adapter layer. The
package’s responsibilities:
- Cross-platform
SandboxManagerabstraction (same API on Seatbelt / bwrap) - Parsing
SandboxRuntimeConfig(sandbox config in settings.json) - Managing
SandboxViolationStore(persistence of violation events) - Unified
SandboxAskCallback(user prompt callback)
Takeaway for your own agent: sandbox capabilities should abstract into a standalone package. Sandbox rules change fast with OS / kernel versions / policy; packaging separately lets sandbox iterate independently without dragging the main product.
Path patterns’ meaning in the sandbox
In Permissions we discussed 4 path patterns (//path / /path / ~/path /
./path). Their actual effect in the sandbox:
//path→ filesystem absolute, e.g.,//tmpallows access to/tmp/path→ relative to settings file directory — ifsettings.jsonis in/home/me/proj/.claude/,/dataexpands to/home/me/proj/.claude/data~/path→ HOME expansion./path→ relative to cwd
These get converted by resolvePathPatternForSandbox (utils/sandbox/sandbox-adapter.ts) into absolute paths
sandbox-runtime understands, then written into bwrap / seatbelt rule files.
Takeaway for your own agent: file path rule syntax should distinguish “absolute” from “relative to config file”. Absolute paths don’t cross machines; config-relative paths do — 80% of dev / CI / production config drift is here.
Process boundary with MCP servers
An easily overlooked boundary: MCP servers are separate processes, not inside Claude Code’s sandbox.
A typical MCP server (e.g., Playwright MCP, GitHub MCP) communicates with Claude Code via stdio or SSE. This means:
- MCP server has its own permissions (regular process permissions on the user’s machine)
- MCP server calling external APIs doesn’t go through Claude Code’s sandbox network policy
- MCP server file reads / writes don’t go through Claude Code’s file sandbox
This is a permission-layer boundary: Claude Code manages invocations of MCP tools via the permission system, but what the MCP server itself does is outside the permission system.
Takeaway for your own agent: subprocesses are new permission boundaries. Your agent can have a perfect internal permission system, but once it spawns a subprocess, the subprocess is fully independent. A common blind spot in compliance audits.
Selection guide for the three modes
| Scenario | Recommended mode | Reason |
|---|---|---|
| Daily coding, editing current repo | Default | Fastest, edits cwd directly |
| Exploratory task, want to see changes without polluting current repo | worktree | Temp git branch, can diff and merge |
| Long-running task (1h+), don’t want to tie up local machine | remote | Runs in background, pull back on completion |
| Needs fully isolated env (e.g., external code audit) | remote | Separate container |
| Batch-process 100 issue-corresponding changes | N × remote | Parallel |
| Already a subagent wanting to spawn another | Default (fork mode) | Avoid nested worktrees |
Takeaways for building your own agent
- “Permissions” and “execution environment” are two orthogonal things — your design needs both, not just
one. Zapvol’s
infra/sandbox/corresponds to this chapter; permission-* to the previous - Isolation should be graded: none (fast) → worktree (file isolation) → remote (full isolation). One-size- fits-all is either too slow or too dangerous
- Worktree slug strict allowlist:
[a-zA-Z0-9._-]+validated per segment, reject.././ absolute paths - Canonical git root: when creating a worktree from inside a worktree, must go back to main repo — otherwise nested worktrees are missed by cleanup
- Hook-based worktree fallback: let users plug in mercurial / sapling / perforce implementations; don’t hard-bind git
- Periodic cleanup + mtime bump: unused worktrees should be cleaned up, but resume paths must refresh mtime to avoid deleting in-use ones
- Sandbox is platform-specific: macOS built-in, Linux needs 3 deps, Windows has none. Deployment docs must spell out these differences
- Componentize SandboxDoctor: make sandbox dependency checks a product-grade UI, not an error message
- Abstract sandbox to a standalone package:
@anthropic-ai/sandbox-runtimelets sandbox rules iterate independently - Cloud execution (Remote) is SaaS integration architecture: GitHub code pull + identity + policy approval — not pure technical isolation; business integration and technical isolation must be designed together
- MCP servers are independent permission domains: your permission system controls invocations of MCP tools; what the MCP server itself does is out of scope — a common compliance blind spot
- Ghost dotfile cleanup details show production sandboxes aren’t simple — every sandbox has leaks / cleanup / edge cases; “exec and forget” doesn’t work
Further reading
- Claude Code source:
tools/AgentTool/AgentTool.tsx,utils/worktree.ts(1519 lines),utils/sandbox/sandbox-adapter.ts,tasks/RemoteAgentTask/RemoteAgentTask.tsx,remote/ - Agent Execution Loop — the Task layer’s 7 task types detailed here
- Permissions — the orthogonal “what can be done” layer
- External package: @anthropic-ai/sandbox-runtime