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.

2
Files
~1 k
Lines
3
Platforms
30+
API Methods

📚 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 / FunctionFilePurpose
SandboxManagersandbox-adapter.tsSingleton implementing ISandboxManager: the main API for all sandbox operations
ISandboxManagersandbox-adapter.ts30+ method interface: initialize, wrapWithSandbox, getFsReadConfig, getNetworkRestrictionConfig, etc.
convertToSandboxRuntimeConfigsandbox-adapter.tsMerges Claude Code settings into SandboxRuntimeConfig for the runtime library
wrapWithSandboxsandbox-adapter.tsWraps a shell command string with sandbox runtime; ensures initialization complete
resolvePathPatternForSandboxsandbox-adapter.tsResolves //path (absolute) and /path (settings-relative) patterns
resolveSandboxFilesystemPathsandbox-adapter.tsResolves filesystem paths from sandbox.filesystem.* settings
shouldAllowManagedSandboxDomainsOnlysandbox-adapter.tsChecks if policy restricts to managed domains only
addToExcludedCommandssandbox-adapter.tsAdds command to unsandboxed exclusion list in localSettings
scrubBareGitRepoFilessandbox-adapter.tsDeletes bare-repo files planted during sandboxed commands (security)
detectWorktreeMainRepoPathsandbox-adapter.tsDetects git worktrees and resolves main repo path for write access
removeSandboxViolationTagssandbox-ui-utils.tsStrips <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.

TypeNameDescription
interfaceISandboxManager30+ method interface for sandbox lifecycle, config, wrapping, and diagnostics
typeSandboxRuntimeConfigFull config: filesystem restrictions, network rules, proxy ports, excluded commands
typeFsReadRestrictionConfigFilesystem read restrictions: allow/deny path patterns
typeFsWriteRestrictionConfigFilesystem write restrictions: allow/deny path patterns
typeNetworkRestrictionConfigNetwork restrictions: allowed domain patterns, proxy settings
typeSandboxDependencyCheckResult of checking platform sandbox dependencies (bubblewrap installed, etc.)
typeSandboxViolationEventRecorded 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.

DirectionPartnerInterfaceDescription
←BashToolwrapWithSandbox(cmd)Every shell command is wrapped before execution
←SettingsconvertToSandboxRuntimeConfig()Reads sandbox config from all 5 settings sources
→@anthropic-ai/sandbox-runtimeBaseSandboxManagerDelegates to the platform-specific sandbox implementation
→PermissionsWebFetch allow rulesExtracts allowed domains from permission rules for network restrictions
→Git utilitiesdetectWorktreeMainRepoPath()Adds worktree main repo to write allowlist
→UIremoveSandboxViolationTags()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 / ParameterDefaultDescription
sandbox.enabledtrue (if supported)Master enable/disable for sandboxing
sandbox.autoAllowBashIfSandboxedfalseAuto-allow bash commands when sandbox is active
sandbox.allowUnsandboxedCommandsfalseAllow excluded commands to run without sandbox
sandbox.enabledPlatformsall supportedRestrict sandbox to specific platforms (undocumented)
sandbox.filesystem.allowWritecwdPaths allowed for write operations
sandbox.filesystem.denyWritesettings filesPaths always denied for writes (hardcoded + config)
sandbox.network.allowedDomains[]Network domains allowed through the sandbox
sandbox.network.allowManagedDomainsOnlyfalsePolicy: 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

Config Merge — Deep Dive">

Settings-to-Config Merge Pipeline Focus

How 5 settings sources, permission rules, and hardcoded protections combine into SandboxRuntimeConfig.

Extracted topic page

Git Bare-Repo Exploit Defense Focus

How scrubBareGitRepoFiles() prevents attacker-planted files from being seen by unsandboxed git operations.

Extracted topic page

Platform-Specific Isolation Focus

macOS sandbox-exec vs Linux/WSL2 bubblewrap implementations, dependency checks, and glob pattern limitations.

Extracted topic page

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.

PlatformBackendStatusNotes
macOSsandbox-exec / bubblewrapSupportedFull filesystem and network isolation
Linuxbubblewrap (bwrap)SupportedGlob patterns may have limitations
WSL2+bubblewrap (bwrap)SupportedRequires WSL2 or later; WSL1 not supported
WSL1—UnsupportedError message with upgrade guidance
Windows (native)—UnsupportedUse 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.

PhaseFunctionDescription
Initializeinitialize()Load settings, detect worktrees, subscribe to settings changes
Config buildconvertToSandboxRuntimeConfig()Merge all sources into SandboxRuntimeConfig
Wrap commandwrapWithSandbox(cmd)Prepend sandbox wrapper to shell command string
Execute(Command Execution Tool" class="xref">BashTool)Run wrapped command via child_process
CleanupcleanupAfterCommand()Scrub bare-repo files, record violations
RefreshrefreshConfig()Sync config after permission updates
Resetreset()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
  }
}