GitHub - anthropic-experimental/sandbox-runtime: A lightweight sandboxing tool for enforcing filesystem and network restrictions on arbitrary processes at the OS level, without requiring a container. (original) (raw)

A lightweight sandboxing tool for enforcing filesystem and network restrictions on arbitrary processes at the OS level, without requiring a container.

srt uses native OS sandboxing primitives (sandbox-exec on macOS, bubblewrap on Linux) and proxy-based network filtering. It can be used to sandbox the behaviour of agents, local MCP servers, bash commands and arbitrary processes.

Beta Research Preview

The Sandbox Runtime is a research preview developed for Claude Code to enable safer AI agents. It's being made available as an early open source preview to help the broader ecosystem build more secure agentic systems. As this is an early research preview, APIs and configuration formats may evolve. We welcome feedback and contributions to make AI agents safer by default!

Installation

npm install -g @anthropic-ai/sandbox-runtime

Basic Usage

Network restrictions

$ srt "curl anthropic.com" Running: curl anthropic.com

... # Request succeeds

$ srt "curl example.com" Running: curl example.com Connection blocked by network allowlist # Request blocked

Filesystem restrictions

$ srt "cat README.md" Running: cat README.md

Anthropic Sandb... # Current directory access allowed

$ srt "cat ~/.ssh/id_rsa" Running: cat ~/.ssh/id_rsa cat: /Users/ollie/.ssh/id_rsa: Operation not permitted # Specific file blocked

Overview

This package provides a standalone sandbox implementation that can be used as both a CLI tool and a library. It's designed with a secure-by-default philosophy tailored for common developer use cases: processes start with minimal access, and you explicitly poke only the holes you need.

Key capabilities:

Example Use Case: Sandboxing MCP Servers

A key use case is sandboxing Model Context Protocol (MCP) servers to restrict their capabilities. For example, to sandbox the filesystem MCP server:

Without sandboxing (.mcp.json):

{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem"] } } }

With sandboxing (.mcp.json):

{ "mcpServers": { "filesystem": { "command": "srt", "args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"] } } }

Then configure restrictions in ~/.srt-settings.json:

{ "filesystem": { "denyRead": [], "allowWrite": ["."], "denyWrite": ["~/sensitive-folder"] }, "network": { "allowedDomains": [], "deniedDomains": [] } }

Now the MCP server will be blocked from writing to the denied path:

> Write a file to ~/sensitive-folder
✗ Error: EPERM: operation not permitted, open '/Users/ollie/sensitive-folder/test.txt'

How It Works

The sandbox uses OS-level primitives to enforce restrictions that apply to the entire process tree:

0d1c612947c798aef48e6ab4beb7e8544da9d41a-4096x2305

Dual Isolation Model

Both filesystem and network isolation are required for effective sandboxing. Without file isolation, a compromised process could exfiltrate SSH keys or other sensitive files. Without network isolation, a process could escape the sandbox and gain unrestricted network access.

Filesystem Isolation enforces read and write restrictions:

Network Isolation (allow-only pattern): By default, all network access is denied. You must explicitly allow domains. An empty allowedDomains list means no network access. Network traffic is routed through proxy servers running on the host:

Both HTTP/HTTPS (via HTTP proxy) and other TCP traffic (via SOCKS5 proxy) are mediated by these proxies, which enforce your domain allowlists and denylists.

For more details on sandboxing in Claude Code, see:

Architecture

src/
├── index.ts                  # Library exports
├── cli.ts                    # CLI entrypoint (srt command)
├── utils/                    # Shared utilities
│   ├── debug.ts             # Debug logging
│   ├── settings.ts          # Settings reader (permissions + sandbox config)
│   ├── platform.ts          # Platform detection
│   └── exec.ts              # Command execution utilities
└── sandbox/                  # Sandbox implementation
    ├── sandbox-manager.ts    # Main sandbox manager
    ├── sandbox-schemas.ts    # Zod schemas for validation
    ├── sandbox-violation-store.ts # Violation tracking
    ├── sandbox-utils.ts      # Shared sandbox utilities
    ├── http-proxy.ts         # HTTP/HTTPS proxy for network filtering
    ├── socks-proxy.ts        # SOCKS5 proxy for network filtering
    ├── linux-sandbox-utils.ts # Linux bubblewrap sandboxing
    └── macos-sandbox-utils.ts # macOS sandbox-exec sandboxing

Usage

As a CLI tool

The srt command (Anthropic Sandbox Runtime) wraps any command with security boundaries:

Run a command in the sandbox

srt echo "hello world"

With debug logging

srt --debug curl https://example.com

Specify custom settings file

srt --settings /path/to/srt-settings.json npm install

As a library

import { SandboxManager, type SandboxRuntimeConfig, } from '@anthropic-ai/sandbox-runtime' import { spawn } from 'child_process'

// Define your sandbox configuration const config: SandboxRuntimeConfig = { network: { allowedDomains: ['example.com', 'api.github.com'], deniedDomains: [], }, filesystem: { denyRead: ['~/.ssh'], allowWrite: ['.', '/tmp'], denyWrite: ['.env'], }, }

// Initialize the sandbox (starts proxy servers, etc.) await SandboxManager.initialize(config)

// Wrap a command with sandbox restrictions const sandboxedCommand = await SandboxManager.wrapWithSandbox( 'curl https://example.com', )

// Execute the sandboxed command const child = spawn(sandboxedCommand, { shell: true, stdio: 'inherit' })

// Handle exit and cleanup after child process completes child.on('exit', async code => { console.log(Command exited with code ${code}) // Cleanup when done (optional, happens automatically on process exit) await SandboxManager.reset() })

Available exports

// Main sandbox manager export { SandboxManager } from '@anthropic-ai/sandbox-runtime'

// Violation tracking export { SandboxViolationStore } from '@anthropic-ai/sandbox-runtime'

// TypeScript types export type { SandboxRuntimeConfig, NetworkConfig, FilesystemConfig, IgnoreViolationsConfig, SandboxAskCallback, FsReadRestrictionConfig, FsWriteRestrictionConfig, NetworkRestrictionConfig, } from '@anthropic-ai/sandbox-runtime'

Configuration

Settings File Location

By default, the sandbox runtime looks for configuration at ~/.srt-settings.json. You can specify a custom path using the --settings flag:

srt --settings /path/to/srt-settings.json

Complete Configuration Example

{ "network": { "allowedDomains": [ "github.com", ".github.com", "lfs.github.com", "api.github.com", "npmjs.org", ".npmjs.org" ], "deniedDomains": ["malicious.com"], "allowUnixSockets": ["/var/run/docker.sock"], "allowLocalBinding": false }, "filesystem": { "denyRead": ["~/.ssh"], "allowRead": [], "allowWrite": [".", "src/", "test/", "/tmp"], "denyWrite": [".env", "config/production.json"] }, "ignoreViolations": { "*": ["/usr/bin", "/System"], "git push": ["/usr/bin/nc"], "npm": ["/private/tmp"] }, "enableWeakerNestedSandbox": false, "enableWeakerNetworkIsolation": false, "allowAppleEvents": false }

Configuration Options

Network Configuration

Uses an allow-only pattern - all network access is denied by default.

Unix Socket Settings (platform-specific behavior):

Setting macOS Linux
allowUnixSockets: string[] Allowlist of socket paths Ignored (seccomp can't filter by path)
allowAllUnixSockets: boolean Allow all sockets Disable seccomp blocking

Unix sockets are blocked by default on both platforms.

Filesystem Configuration

Uses two different patterns:

Read restrictions (deny-then-allow pattern) - all reads allowed by default:

Write restrictions (allow-only pattern) - all writes denied by default:

Path Syntax (macOS):

Paths support git-style glob patterns on macOS, similar to .gitignore syntax:

Examples:

Path Syntax (Linux):

Linux currently does not support glob matching. Use literal paths only:

All platforms:

Other Configuration

Common Configuration Recipes

Allow GitHub access (all necessary endpoints):

{ "network": { "allowedDomains": [ "github.com", "*.github.com", "lfs.github.com", "api.github.com" ], "deniedDomains": [] }, "filesystem": { "denyRead": [], "allowWrite": ["."], "denyWrite": [] } }

Restrict to specific directories:

{ "network": { "allowedDomains": [], "deniedDomains": [] }, "filesystem": { "denyRead": ["~/.ssh"], "allowWrite": [".", "src/", "test/"], "denyWrite": [".env", "secrets/"] } }

Workspace-only filesystem access (deny reads outside the workspace):

{ "network": { "allowedDomains": [], "deniedDomains": [] }, "filesystem": { "denyRead": ["/Users"], "allowRead": ["."], "allowWrite": ["."], "denyWrite": [] } }

This denies reading anything under /Users (or /home on Linux), then re-allows the current working directory. System paths (/usr, /lib, etc.) remain readable.

Common Issues and Tips

Running Jest: Use --no-watchman flag to avoid sandbox violations:

Watchman accesses files outside the sandbox boundaries, which will trigger permission errors. Disabling it allows Jest to run with the built-in file watcher instead.

Platform Support

Platform-Specific Dependencies

Linux requires:

Ubuntu 24.04+ note: These releases enable kernel.apparmor_restrict_unprivileged_userns by default, which allows unshare(CLONE_NEWUSER) but strips capabilities from the resulting namespace. Both bubblewrap and the seccomp isolation layer need capability-bearing user namespaces. Disable the restriction with:

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

or add an AppArmor profile that grants userns to the relevant binaries.

Optional Linux dependencies (for seccomp fallback):

The package includes pre-generated seccomp BPF filters for x86-64 and arm architectures. These dependencies are only needed if you are on a different architecture where pre-generated filters are not available:

macOS requires:

Development

Install dependencies

npm install

Build the project

npm run build

Run tests

npm test

Type checking

npm run typecheck

Lint code

npm run lint

Format code

npm run format

Building Seccomp Binaries

The BPF filter and apply-seccomp loader are compiled from C source in vendor/seccomp-src/ via npm run build:seccomp (Linux only; needs gcc and libseccomp-dev). CI runs it before tests on each Linux arch, and the release workflow builds both arches and bundles them into the published package.

Implementation Details

Network Isolation Architecture

The sandbox runs HTTP and SOCKS5 proxy servers on the host machine that filter all network requests based on permission rules:

  1. HTTP/HTTPS Traffic: An HTTP proxy server intercepts requests and validates them against allowed/denied domains
  2. Other Network Traffic: A SOCKS5 proxy handles all other TCP connections (SSH, database connections, etc.)
  3. Permission Enforcement: The proxies enforce the permissions rules from your configuration

Platform-specific proxy communication:

Filesystem Isolation

Filesystem restrictions are enforced at the OS level:

Default filesystem permissions:

Precedence is intentionally opposite for reads vs writes: allowRead overrides denyRead, while denyWrite overrides allowWrite. This lets you carve out readable regions within denied areas, and carve out protected regions within writable areas.

Mandatory Deny Paths (Auto-Protected Files)

Certain sensitive files and directories are always blocked from writes, even if they fall within an allowed write path. This provides defense-in-depth against sandbox escapes and configuration tampering.

Always-blocked files:

Always-blocked directories:

These paths are blocked automatically - you don't need to add them to denyWrite. For example, even with allowWrite: ["."], writing to .bashrc or .git/hooks/pre-commit will fail:

$ srt 'echo "malicious" >> .bashrc' /bin/bash: .bashrc: Operation not permitted

$ srt 'echo "bad" > .git/hooks/pre-commit' /bin/bash: .git/hooks/pre-commit: Operation not permitted

Note (Linux): On Linux, mandatory deny paths only block files that already exist. Non-existent files in these patterns cannot be blocked by bubblewrap's bind-mount approach. macOS uses glob patterns which block both existing and new files.

Linux search depth: On Linux, the sandbox uses ripgrep to scan for dangerous files in subdirectories within allowed write paths. By default, it searches up to 3 levels deep for performance. You can configure this with mandatoryDenySearchDepth:

{ "mandatoryDenySearchDepth": 5, "filesystem": { "allowWrite": ["."] } }

Unix Socket Restrictions (Linux)

On Linux, the sandbox uses seccomp BPF (Berkeley Packet Filter) to block Unix domain socket creation at the syscall level. This provides an additional layer of security to prevent processes from creating new Unix domain sockets for local IPC (unless explicitly allowed).

How it works:

  1. Baked-in BPF filter: The package ships a static apply-seccomp binary for x64 and arm64 with the seccomp BPF filter compiled in. The filter is architecture-specific but libc-independent, so the binary works with both glibc and musl.
  2. Runtime detection: The sandbox automatically detects your system's architecture and uses the matching apply-seccomp binary.
  3. Syscall filtering: The BPF filter intercepts the socket() syscall and blocks creation of AF_UNIX sockets by returning EPERM. This prevents sandboxed code from creating new Unix domain sockets.
  4. Two-stage application using apply-seccomp binary:
    • Outer bwrap creates the sandbox with filesystem, network, and PID namespace restrictions
    • Network bridging processes (socat) start inside the sandbox (need Unix sockets)
    • apply-seccomp creates a nested user+PID+mount namespace and remounts /proc
    • Inside the nested namespace, apply-seccomp acts as PID 1 (non-dumpable init/reaper)
    • apply-seccomp forks, applies the seccomp filter via prctl(), and execs the user command
    • User command runs with all sandbox restrictions plus Unix socket creation blocking

PID namespace isolation: The nested PID namespace ensures the user command cannot see or address any process that runs without the seccomp filter (bwrap's init, the shell wrapper, or the socat helpers). This keeps the seccomp boundary intact regardless of kernel.yama.ptrace_scope, since unfiltered helpers are not reachable via ptrace or /proc/N/mem. The inner PID 1 sets PR_SET_DUMPABLE=0 so it is not ptraceable either. If nested namespace creation fails, apply-seccomp aborts rather than running without isolation.

Security limitations: The filter blocks socket(AF_UNIX, ...) and the io_uring_setup/io_uring_enter/io_uring_register syscalls (the latter three because IORING_OP_SOCKET on Linux 5.19+ would otherwise bypass the socket() rule). It does not prevent operations on Unix socket file descriptors inherited from parent processes or passed via SCM_RIGHTS. For most sandboxing scenarios, blocking socket creation is sufficient to prevent unauthorized IPC.

Zero runtime dependencies: Pre-built static apply-seccomp binaries and pre-generated BPF filters are included for x64 and arm64 architectures. No compilation tools or external dependencies required at runtime.

Architecture support: x64 and arm64 are fully supported with pre-built binaries. Other architectures are not currently supported. To use sandboxing without Unix socket blocking on unsupported architectures, set allowAllUnixSockets: true in your configuration.

Violation Detection and Monitoring

When a sandboxed process attempts to access a restricted resource:

  1. Blocks the operation at the OS level (returns EPERM error)
  2. Logs the violation (platform-specific mechanisms)
  3. Notifies the user (in Claude Code, this triggers a permission prompt)

macOS: The sandbox runtime taps into macOS's system sandbox violation log store. This provides real-time notifications with detailed information about what was attempted and why it was blocked. This is the same mechanism Claude Code uses for violation detection.

View sandbox violations in real-time

log stream --predicate 'process == "sandbox-exec"' --style syslog

Linux: Bubblewrap doesn't provide built-in violation reporting. Use strace to trace system calls and identify blocked operations:

Trace all denied operations

strace -f srt 2>&1 | grep EPERM

Trace specific file operations

strace -f -e trace=open,openat,stat,access srt 2>&1 | grep EPERM

Trace network operations

strace -f -e trace=network srt 2>&1 | grep EPERM

Advanced: Bring Your Own Proxy

For more sophisticated network filtering, you can configure the sandbox to use your own proxy instead of the built-in ones. This enables:

Example with mitmproxy:

Start mitmproxy with custom filtering script

mitmproxy -s custom_filter.py --listen-port 8888

Note: Custom proxy configuration is not yet supported in the new configuration format. This feature will be added in a future release.

Important security consideration: Even with domain allowlists, exfiltration vectors may exist. For example, allowing github.com lets a process push to any repository. With a custom MITM proxy and proper certificate setup, you can inspect and filter specific API calls to prevent this.

Security Limitations

Known Limitations and Future Work

Linux proxy bypass: Currently uses environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) to direct traffic through proxies. This works for most applications but may be ignored by programs that don't respect these variables, leading to them being unable to connect to the internet.

Future improvements: