utils/sandbox/ — Sandbox Isolation System
OS-level isolation for tool execution restricting filesystem, network, and command access
The sandbox module wraps the @anthropic-ai/sandbox-runtime library with Claude Code-specific settings integration. It enforces filesystem read/write restrictions, network domain allowlists, git bare-repo exploit protection, and platform-specific sandboxing via bubblewrap on Linux/WSL and sandbox-exec on macOS. Settings from 5 sources are merged into a unified SandboxRuntimeConfig.
📚 What is this & why does it exist?
The sandbox module provides OS-level containment for every shell command the AI runs. When you ask Claude Code to execute npm install, the command doesn't run with your full user permissions—it runs inside a restricted environment where it can only touch files in your project, access approved network domains, and cannot modify system-critical files. This prevents accidental (or malicious) damage even if the AI makes a mistake.
💡 Real-world analogy
Think of a chemical laboratory's fume hood. Scientists (tools) can perform experiments (commands) inside the hood, but the hood contains any dangerous fumes (filesystem writes, network access) and prevents them from reaching the rest of the lab (system). The hood has adjustable vents (configuration) that can be opened for specific chemicals (allowed domains/paths) while keeping everything else contained. Some experiments are too dangerous even for the hood (excluded commands) and must be performed in the open with explicit supervisor approval.
Contents
Architecture
The sandbox adapter sits between Claude Code's settings system and the @anthropic-ai/sandbox-runtime library. It translates permission rules and settings from 5 sources into a unified SandboxRuntimeConfig, adds hardcoded security protections, and manages the sandbox lifecycle including initialization, command wrapping, and post-execution cleanup.
flowchart TD BASH["BashTool\n(Command execution)"] --> SM["SandboxManager\n(Adapter layer)"] SM --> BSM["BaseSandboxManager\n(@anthropic-ai/sandbox-runtime)"] SM --> CONV["convertToSandboxRuntimeConfig()"] CONV --> SETTINGS["Settings Sources"] SETTINGS --> US["userSettings"] SETTINGS --> PS["projectSettings"] SETTINGS --> LS["localSettings"] SETTINGS --> FS["flagSettings"] SETTINGS --> POL["policySettings"] CONV --> FSR["Filesystem Restrictions\n(allowWrite/denyWrite/allowRead/denyRead)"] CONV --> NR["Network Restrictions\n(allowedDomains from WebFetch rules)"] CONV --> GP["Git Protection\n(bare-repo exploit defense)"] BSM --> MAC["macOS\n(sandbox-exec)"] BSM --> LIN["Linux / WSL2+\n(bubblewrap)"] SM --> VS["SandboxViolationStore\n(Violation tracking)"] SM --> CLEAN["scrubBareGitRepoFiles()\n(Post-command cleanup)"]
Key Classes & Functions
The sandbox module exposes its functionality primarily through the SandboxManager singleton which implements the ISandboxManager interface. All tool execution flows through wrapWithSandbox().
| Class / Function | File | Purpose |
|---|---|---|
SandboxManager | sandbox-adapter.ts | Singleton implementing ISandboxManager: the main API for all sandbox operations |
ISandboxManager | sandbox-adapter.ts | 30+ method interface: initialize, wrapWithSandbox, getFsReadConfig, getNetworkRestrictionConfig, etc. |
convertToSandboxRuntimeConfig | sandbox-adapter.ts | Merges Claude Code settings into SandboxRuntimeConfig for the runtime library |
wrapWithSandbox | sandbox-adapter.ts | Wraps a shell command string with sandbox runtime; ensures initialization complete |
resolvePathPatternForSandbox | sandbox-adapter.ts | Resolves //path (absolute) and /path (settings-relative) patterns |
resolveSandboxFilesystemPath | sandbox-adapter.ts | Resolves filesystem paths from sandbox.filesystem.* settings |
shouldAllowManagedSandboxDomainsOnly | sandbox-adapter.ts | Checks if policy restricts to managed domains only |
addToExcludedCommands | sandbox-adapter.ts | Adds command to unsandboxed exclusion list in localSettings |
scrubBareGitRepoFiles | sandbox-adapter.ts | Deletes bare-repo files planted during sandboxed commands (security) |
detectWorktreeMainRepoPath | sandbox-adapter.ts | Detects git worktrees and resolves main repo path for write access |
removeSandboxViolationTags | sandbox-ui-utils.ts | Strips <sandbox_violations> XML tags from error text for UI display |
Data Structures
The sandbox module re-exports several types from @anthropic-ai/sandbox-runtime and defines the ISandboxManager interface for the adapter pattern.
| Type | Name | Description |
|---|---|---|
| interface | ISandboxManager | 30+ method interface for sandbox lifecycle, config, wrapping, and diagnostics |
| type | SandboxRuntimeConfig | Full config: filesystem restrictions, network rules, proxy ports, excluded commands |
| type | FsReadRestrictionConfig | Filesystem read restrictions: allow/deny path patterns |
| type | FsWriteRestrictionConfig | Filesystem write restrictions: allow/deny path patterns |
| type | NetworkRestrictionConfig | Network restrictions: allowed domain patterns, proxy settings |
| type | SandboxDependencyCheck | Result of checking platform sandbox dependencies (bubblewrap installed, etc.) |
| type | SandboxViolationEvent | Recorded sandbox violation with command, path, and violation type |
Interactions
The sandbox module is a critical integration point between tool execution, settings, permissions, and the underlying OS isolation mechanism.
| Direction | Partner | Interface | Description |
|---|---|---|---|
| ← | BashTool | wrapWithSandbox(cmd) | Every shell command is wrapped before execution |
| ← | Settings | convertToSandboxRuntimeConfig() | Reads sandbox config from all 5 settings sources |
| → | @anthropic-ai/sandbox-runtime | BaseSandboxManager | Delegates to the platform-specific sandbox implementation |
| → | Permissions | WebFetch allow rules | Extracts allowed domains from permission rules for network restrictions |
| → | Git utilities | detectWorktreeMainRepoPath() | Adds worktree main repo to write allowlist |
| → | UI | removeSandboxViolationTags() | Cleans violation XML from stderr before displaying to user |
Configuration Merge Flow
The sandbox config merge pipeline is one of the most complex parts of the module. It must reconcile settings from 5 sources with different precedence levels, add hardcoded security protections, and produce a single SandboxRuntimeConfig that the underlying sandbox runtime can consume.
flowchart LR US["userSettings\n(~/.claude/settings.json)"] --> MERGE["convertToSandboxRuntimeConfig()"] PS["projectSettings\n(.claude/settings.json)"] --> MERGE LS["localSettings\n(.claude/settings.local.json)"] --> MERGE FS["flagSettings\n(feature flags)"] --> MERGE POL["policySettings\n(policy overrides)"] --> MERGE MERGE --> FW["filesystem.allowWrite[]"] MERGE --> FD["filesystem.denyWrite[]"] MERGE --> FR["filesystem.allowRead[]"] MERGE --> ND["network.allowedDomains[]"] MERGE --> PROT["Hardcoded Protections"] PROT --> S1["Deny write: settings.json"] PROT --> S2["Deny write: .claude/skills"] PROT --> S3["Deny write: git bare-repo paths"] PROT --> S4["Allow write: worktree main repo"]
Configuration
The sandbox exposes several settings knobs that control isolation behavior. Most are configured via the Claude Code settings files, with some policy-level overrides that cannot be changed by individual users.
| Knob / Parameter | Default | Description |
|---|---|---|
sandbox.enabled | true (if supported) | Master enable/disable for sandboxing |
sandbox.autoAllowBashIfSandboxed | false | Auto-allow bash commands when sandbox is active |
sandbox.allowUnsandboxedCommands | false | Allow excluded commands to run without sandbox |
sandbox.enabledPlatforms | all supported | Restrict sandbox to specific platforms (undocumented) |
sandbox.filesystem.allowWrite | cwd | Paths allowed for write operations |
sandbox.filesystem.denyWrite | settings files | Paths always denied for writes (hardcoded + config) |
sandbox.network.allowedDomains | [] | Network domains allowed through the sandbox |
sandbox.network.allowManagedDomainsOnly | false | Policy: restrict to only managed domains (no user overrides) |
Directory Map
Source tree for sandbox
utils/sandbox/
├── sandbox-adapter.ts ← Main adapter: ISandboxManager, config conversion, 985 lines
└── sandbox-ui-utils.ts ← UI utility: removeSandboxViolationTags, 12 lines
Deep-Dive Highlights
Core mechanisms from this sub-module have been extracted into dedicated focus pages for in-depth exploration. Each page covers one algorithm, protocol, or subsystem with diagrams, code walks, and configuration details.
Deep-Dive Pages
Settings-to-Config Merge Pipeline Focus
How 5 settings sources, permission rules, and hardcoded protections combine into SandboxRuntimeConfig.
Git Bare-Repo Exploit Defense Focus
How scrubBareGitRepoFiles() prevents attacker-planted files from being seen by unsandboxed git operations.
Platform-Specific Isolation Focus
macOS sandbox-exec vs Linux/WSL2 bubblewrap implementations, dependency checks, and glob pattern limitations.
Platform Support
The sandbox supports three major platforms through the bubblewrap isolation backend. Platform detection is memoized and cached for the lifetime of the process.
| Platform | Backend | Status | Notes |
|---|---|---|---|
| macOS | sandbox-exec / bubblewrap | Supported | Full filesystem and network isolation |
| Linux | bubblewrap (bwrap) | Supported | Glob patterns may have limitations |
| WSL2+ | bubblewrap (bwrap) | Supported | Requires WSL2 or later; WSL1 not supported |
| WSL1 | — | Unsupported | Error message with upgrade guidance |
| Windows (native) | — | Unsupported | Use WSL2 for sandbox support |
Security Protections
Settings File Escape Prevention
The sandbox unconditionally denies write access to all settings files (.claude/settings.json, .claude/settings.local.json, etc.) across all 5 sources. This is the single most important security measure in the sandbox—it prevents a sandboxed command from modifying its own permissions by editing the settings file that controls the sandbox configuration. Even if a command escapes other restrictions, it cannot weaken the sandbox itself.
Git Bare-Repo Exploit Defense
An attacker could plant files like HEAD, objects/, refs/, hooks/, and config in the working directory during a sandboxed command. If git later runs outside the sandbox, it might interpret these as a bare git repository and execute hooks. The scrubBareGitRepoFiles() function deletes these planted files after every sandboxed command as a defense-in-depth measure. The function maintains a list of bare-repo paths (bareGitRepoScrubPaths) that are checked and cleaned during cleanupAfterCommand().
Worktree Write Access
When the working directory is a git worktree, the sandbox automatically adds the main repository path to the write allowlist. This enables symmetric access—changes made in a worktree can affect shared refs and objects in the main repository, which is essential for operations like git commit and git push.
Skills Directory Protection
The .claude/skills directory is always deny-listed for writes to prevent sandboxed commands from tampering with installed skills or planting malicious skill files.
Managed Domain Policy
When policySettings.sandbox.network.allowManagedDomainsOnly is true, the sandbox ask callback is wrapped to enforce that only managed (pre-approved) domains can be added. User-requested domain additions are silently blocked. This is a policy-level control for enterprise deployments that need to restrict outbound network access to approved endpoints only, preventing data exfiltration through unauthorized domains.
Sandbox Lifecycle
The sandbox follows a well-defined lifecycle from initialization through command execution to cleanup. The lifecycle ensures that settings changes are picked up dynamically and that post-execution cleanup always runs, even if the command fails.
| Phase | Function | Description |
|---|---|---|
| Initialize | initialize() | Load settings, detect worktrees, subscribe to settings changes |
| Config build | convertToSandboxRuntimeConfig() | Merge all sources into SandboxRuntimeConfig |
| Wrap command | wrapWithSandbox(cmd) | Prepend sandbox wrapper to shell command string |
| Execute | (Command Execution Tool" class="xref">BashTool) | Run wrapped command via child_process |
| Cleanup | cleanupAfterCommand() | Scrub bare-repo files, record violations |
| Refresh | refreshConfig() | Sync config after permission updates |
| Reset | reset() | Clear subscriptions, caches, and state |
Code Examples
Example: Path Pattern Resolution for Sandbox
// Resolves Claude Code-specific path patterns for sandbox-runtime
// Handles two special prefixes before delegating to the runtime library
export function resolvePathPatternForSandbox(
pattern: string,
source: SettingSource,
): string {
// "//path" → absolute from filesystem root ("/path")
if (pattern.startsWith('//')) {
return pattern.slice(1);
}
// "/path" → relative to the settings file directory
if (pattern.startsWith('/')) {
const settingsDir = getSettingsDirForSource(source);
return join(settingsDir, pattern.slice(1));
}
// "~/path", "./path", "path" → passed through for sandbox-runtime
return pattern;
}
Example: Hardcoded Write Protections
// convertToSandboxRuntimeConfig() always adds these write denials:
const hardcodedDenyWrite: string[] = [];
// Protect settings files from sandbox escape
for (const source of SETTING_SOURCES) {
hardcodedDenyWrite.push(getSettingsFilePath(source));
}
// Protect .claude/skills from tampering
hardcodedDenyWrite.push(join(cwd, '.claude', 'skills'));
// Protect against bare git repo exploits
// (attacker plants HEAD, objects/, refs/, hooks/, config)
const bareGitPaths = ['HEAD', 'objects', 'refs', 'hooks', 'config'];
for (const p of bareGitPaths) {
hardcodedDenyWrite.push(join(cwd, p));
}
// Add worktree main repo to allowlist for symmetric access
if (worktreeMainRepoPath) {
config.filesystem.allowWrite.push(worktreeMainRepoPath);
}
Example: Detecting Git Worktree for Sandbox
// Detects if cwd is a git worktree and returns the main repo path
// so the sandbox can add it to the write allowlist
async function detectWorktreeMainRepoPath(
cwd: string,
): Promise<string | null> {
const dotGitPath = join(cwd, '.git');
try {
const content = await readFile(dotGitPath, 'utf8');
// Regular repos: .git is a directory (not a file)
// Worktrees: .git is a file containing "gitdir: path"
if (!content.startsWith('gitdir: ')) return null;
const gitDir = content.slice('gitdir: '.length).trim();
// Resolve relative paths against cwd
const absoluteGitDir = resolve(cwd, gitDir);
// Walk up from the gitdir to find the main repo
// e.g., /repo/.git/worktrees/feature -> /repo
const mainGitDir = join(absoluteGitDir, '..', '..');
return resolve(mainGitDir, '..');
} catch {
return null; // Not a worktree or .git doesn't exist
}
}