DEV Community

Cover image for Claude Code's Sandbox Is an npm Package. Run srt Yourself and Watch What It Blocks
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

Claude Code's Sandbox Is an npm Package. Run srt Yourself and Watch What It Blocks


Turn on Claude Code's sandbox and ask Claude to run cat ~/.ssh/known_hosts. The command goes through. That isn't a bug. Claude Code's sandboxing docs say a sandboxed command can read "most of the machine, including credential files such as ~/.ssh and ~/.aws/credentials" until you deny them.

The same page has a line most people scroll past: "Claude Code builds the sandbox on the open source @anthropic-ai/sandbox-runtime package." That package ships a CLI called srt. You can run it without Claude Code, wrap any command in it, and see for yourself which layer refuses what. It's a fast way to learn what /sandbox promises before you let an agent run with fewer prompts.

@anthropic-ai/sandbox-runtime is Apache-2.0, and its README calls it a "Beta Research Preview" whose "APIs and configuration formats may evolve." The npm registry lists version 0.0.80, which needs Node.js 22.12.0 or newer.

There is no Docker image and no VM. On macOS it runs your command through sandbox-exec with a Seatbelt profile it generates. On Linux it uses bubblewrap. Windows support exists and the README marks it alpha. The network goes through two proxies, an HTTP proxy and a SOCKS5 proxy, that run on the host outside the sandbox and check every host against your lists. The sandboxed process has no other way out: on Linux "the network namespace of the sandboxed process is removed entirely," and on macOS "the Seatbelt profile allows communication only to a specific localhost port."

Anthropic introduced it in Beyond permission prompts on October 20, 2025. The post says the limits cover "any scripts, programs, or subprocesses that are spawned by the command," and that "in our internal usage" sandboxing cut permission prompts by 84%. That figure is Anthropic's own measurement, not an independent one.

Install it and write a policy

npm install -g @anthropic-ai/sandbox-runtime
# macOS: srt looks for ripgrep on your PATH
brew install ripgrep
# Debian and Ubuntu
sudo apt-get install bubblewrap socat ripgrep
Enter fullscreen mode Exit fullscreen mode

ripgrep is how srt finds the paths it must deny, and socat relays traffic to the proxies on Linux. Install srt globally, not into your project. The README's security notes give the reason: a copy in <project>/node_modules/ with the project writable "can be rewritten by a sandboxed command, and the next srt there runs the result unsandboxed." On Ubuntu 24.04 and later, AppArmor stops bubblewrap from creating user namespaces by default, and the Claude Code docs have the profile that allows it.

With no settings file, srt starts anyway, with no network and writes confined to a few runtime paths such as /tmp/claude. Claude Code's sandbox environments page puts it plainly: "Don't take a clean start as proof your settings loaded." Save a policy as ~/.srt-settings.json:

{
  "network": {
    "allowedDomains": ["registry.npmjs.org"],
    "deniedDomains": []
  },
  "filesystem": {
    "denyRead": ["~/.ssh"],
    "allowWrite": ["."],
    "denyWrite": []
  }
}
Enter fullscreen mode Exit fullscreen mode

The config schema requires all five lists, even the empty ones. If the file exists but is empty or invalid, srt refuses to start, which is what you want from a security tool. Reads are allowed everywhere unless you deny them. Writes are denied everywhere unless you allow them. Network is denied unless the host is on allowedDomains, and deniedDomains is checked first.

One probe, run twice

Make a throwaway git repo, so there is a .git/hooks folder to aim at, and save this in it as probe.sh:

#!/usr/bin/env bash
# probe.sh: run it plain first, then under srt
check() {
  if "$@" >/dev/null 2>&1; then echo "ok    $*"
  else echo "FAIL  $*"; fi
}
check cat "$HOME/.ssh/known_hosts"
check touch "$HOME/srt-probe"
check touch ./srt-probe
check touch .git/hooks/srt-probe
check curl -fsS https://registry.npmjs.org/left-pad
check curl -fsS https://example.com
check curl -fsS --noproxy '*' https://registry.npmjs.org/left-pad
Enter fullscreen mode Exit fullscreen mode
mkdir -p ~/code/srt-lab && cd ~/code/srt-lab && git init
# save probe.sh here, then:
bash probe.sh
srt bash probe.sh
rm -f ~/srt-probe ./srt-probe .git/hooks/srt-probe
Enter fullscreen mode Exit fullscreen mode

The plain run is your baseline. Every line should say ok, unless you have no known_hosts file. In the second run, srt starts bash, and every cat, touch and curl that bash starts inherits the same limits.

Pass the command as separate words, as above. In the current CLI source, positional arguments are shell-quoted before srt runs them with bash -c, so srt "bash probe.sh" would look for a program literally named bash probe.sh. For a line with pipes, redirects or ~, use srt -c '...', which the source describes as "run command string directly (like sh -c), no escaping applied."

Here is what each line should do under srt, going by the README and Claude Code's docs. I'm working from those, not from a run of my own, so trust your terminal over this table:

Probe Under srt What stops it
cat ~/.ssh/known_hosts FAIL denyRead, enforced by the OS
touch ~/srt-probe FAIL Writes are allow-only, home isn't listed
touch ./srt-probe ok allowWrite: ["."]
touch .git/hooks/srt-probe FAIL Built-in deny, even inside .
curl registry.npmjs.org ok Host is allowed, proxy passes it
curl example.com FAIL Proxy refuses the host
curl --noproxy '*' FAIL OS layer: no route but the proxy

The README shows a denied read failing with Operation not permitted. For a write outside the boundary, Claude Code's docs list Operation not permitted on macOS and Read-only file system on Linux and WSL2, and they show a --noproxy curl failing with Could not resolve host. A refused HTTP request gets a 403 from the proxy with the header X-Proxy-Error: blocked-by-sandbox-runtime, per the README.

Two layers, and the gap between them

The last two curl lines are the whole design. The proxy decides by hostname. Claude Code's docs say it "checks the hostname of each connection against your allowed and denied domains" and, by default, "does not terminate or inspect TLS." They also warn that broad entries such as github.com "can create paths for data exfiltration," and the README adds that domain fronting may get past the filter. Treat every allowed domain as a place your data can go.

The OS layer is why ignoring the proxy doesn't help. The proxy reaches tools through HTTP_PROXY, HTTPS_PROXY and ALL_PROXY. A tool that ignores them has no route at all, which is also why, per Claude Code's docs, plain ssh and most database drivers "can't connect, even to an allowed host," and UDP, QUIC and ping can't leave either.

The .git/hooks line shows the other built-in rule. Some paths are write-denied even inside an allowed folder, among them shell startup files, .gitconfig, .git/hooks, .git/config, .vscode, .idea, .mcp.json, .claude/commands and .claude/agents. Those are the files where a sandboxed command could plant code that something unsandboxed runs later. One caveat from the sandbox environments page: on Linux the runtime builds that deny list once at launch, so it doesn't cover a repo the session creates later with git init or git clone.

Inside Claude Code, /sandbox wraps shell commands only. The docs list what runs outside it: the Read, Edit, WebFetch and WebSearch tools, hooks, local MCP servers and LSP servers. "A denyRead entry doesn't stop the Read tool." The sandbox is also off by default, and if a dependency is missing, Claude Code runs commands unsandboxed unless you tell it to stop. Put this in ~/.claude/settings.json to close those doors:

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "filesystem": { "denyRead": ["~/.ssh", "~/.aws"] }
  }
}
Enter fullscreen mode Exit fullscreen mode

allowUnsandboxedCommands: false turns off the retry where Claude asks to rerun a failed command outside the sandbox. For the Read tool you still need a deny rule in permissions. To put hooks and MCP servers behind the same wall, the docs suggest running all of Claude Code under srt with npx @anthropic-ai/sandbox-runtime claude. The sandbox environments page lists the paths and domains your srt settings must allow first.

Put srt around your own agent's tool calls

srt is also a library. If you build an agent in TypeScript and give the model a shell tool, wrap the tool's handler. This follows the README's library example:

// sandboxed-shell.mts
import { spawn } from 'node:child_process'
import {
  SandboxManager,
  type SandboxRuntimeConfig,
} from '@anthropic-ai/sandbox-runtime'

const config: SandboxRuntimeConfig = {
  network: {
    allowedDomains: ['registry.npmjs.org'],
    deniedDomains: [],
  },
  filesystem: {
    denyRead: ['~/.ssh'],
    allowWrite: ['.'],
    denyWrite: [],
  },
}

await SandboxManager.initialize(config)
Enter fullscreen mode Exit fullscreen mode

initialize() starts the proxies once, at startup. The tool handler goes in the same file:

// Handler for a "run_shell" tool the model can call
export async function runShell(cmd: string): Promise<string> {
  const wrapped = await SandboxManager.wrapWithSandbox(cmd)
  return new Promise((resolve) => {
    const child = spawn(wrapped, { shell: true })
    let out = ''
    child.stdout.on('data', (d) => (out += d))
    child.stderr.on('data', (d) => (out += d))
    child.on('close', (code) => resolve(`exit ${code}\n${out}`))
  })
}

// On shutdown: await SandboxManager.reset()
Enter fullscreen mode Exit fullscreen mode

wrapWithSandbox() returns the command line that runs your string inside the boundary. The model gets Operation not permitted back as the tool result and can change course, and nobody has to click Approve. Run the agent from a workspace folder that isn't the folder holding your app and its node_modules, for the same reason the README gives about installing srt.

Where Hermes IDE fits

I build Hermes IDE, so weigh this section with that in mind. By default, Hermes opens each agent in a real terminal running the agent's own interface. /sandbox is Claude Code's own command, so in that terminal it opens the same panel and enforces the same boundary. Hermes also has an opt-in Agent view for Claude that renders the conversation in place of Claude's terminal interface, and this post doesn't cover that mode. Codex brings its own sandbox, and OpenAI's sandboxing page says it uses Seatbelt on macOS and bubblewrap on Linux and WSL2 too.

Codex and Claude Code sessions side by side in Hermes IDE, each in its own hermes/ worktree, with the sessions sidebar on the left
Two real agent sessions side by side: Codex on orders-types and Claude Code on orders-api, each in its own hermes/ worktree, with the sessions sidebar (status, context use, cost, branch) on the left.

Hermes adds no sandbox of its own. A worktree per task keeps agents out of each other's files, and your home directory is still the sandbox's job. Claude Code's sandbox does know about worktrees: in a linked worktree it also allows writes to the main repository's shared .git, so git commit works, while hooks/ and config in there stay write-denied.

When the sandbox needs a decision, such as a new domain in manual or accept-edits mode, or an unsandboxed retry, the prompt appears in Claude Code's terminal. For an agent blocked on a prompt like that, Hermes shows the session as needing approval (and says when that status is a guess), the attention inbox counts the agents blocked on you, and ⌘I (Ctrl+Shift+I on Windows and Linux) jumps to the agent that has waited longest. You answer in that agent's own terminal. Hermes never answers a permission prompt for you and never types into a terminal on its own.

The Hermes IDE New task sheet with a refund-endpoint task typed, chips for Claude Code, the project, a new worktree, Accept edits, model and effort, and a preview of the claude command Hermes will run
The ⌘N New task sheet: Claude Code, the orders-api project, a new hermes/ worktree, Accept edits, model and effort, and the "Hermes will run" preview of the claude command.

Hermes also runs any command-line agent you add, so npx @anthropic-ai/sandbox-runtime claude can be the command, once your srt settings allow what Claude Code needs. I haven't run that combination myself.


If this was useful

The runShell handler above is the moment an agent stops talking and starts acting, and that's what AI That Acts is about: tool calling, functions and a first working agent, in TypeScript. Read it for the tool loop, then put srt around the tool that touches your machine.

AI That Acts — AI in TypeScript

It's book 3 of AI in TypeScript, five books from your first LLM call to agents in production: all five here.

Hermes IDE is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. Download it or star it on GitHub. It isn't affiliated with Anthropic, OpenAI, Nous Research or any other agent vendor.

Top comments (0)