Sandboxing a coding agent
You ask a coding agent why the app can’t reach its database. It opens
.env to check the connection settings, and the database password is now
in the tool result. Claude Code reads files in the working folder without
asking you first [1], so it didn’t ask you. The tool
result is part of what the agent sends to the model provider with its next
request. A sandbox changes nothing about that traffic
[2]. The password has left your machine, and nobody
decided to send it.
Keeping API keys out of the agent’s reach was about secrets going in to the agent: which key it holds and where that key is kept. This lesson is about what can go out. We compare the ways to contain a coding agent and look at why the controls that matter most still hold when the agent has been convinced to do something else. Then we set up Claude Code’s built-in sandbox with a network allowlist, and you predict which commands that allowlist lets through.
Ways to contain an agent
Section titled “Ways to contain an agent”A sandbox is the environment the agent runs in, set up so that a wrong or hostile command reaches only what you put inside it. Each kind trades setup work for control:
| Approach | For | Against |
|---|---|---|
| The agent’s built-in sandbox | It comes with the agent, and on macOS there is nothing to install | Only the shell commands the agent runs are inside it. Hooks, MCP (Model Context Protocol) servers and the agent’s own file tools stay on your computer |
| The vendor’s sandbox runtime | Hooks and MCP servers are inside the boundary too, because the entire agent runs in the operating system isolation of the built-in sandbox. Docker isn’t needed | It is an early beta, and the way you configure it may still change |
| A hardened container (Docker, Podman) | You choose the mounts, the secrets that go in and the hosts it may reach | Medium to high setup effort |
| A hardened dev container | The editor starts and manages it, and the vendor’s example comes with a firewall that blocks traffic to hosts it doesn’t list | The team keeps the image and the firewall’s list current. Using it is a team convention, and a developer can still start the agent on the host |
| A hardened virtual machine | A kernel of its own between the agent and your computer, which keeps the two further apart than any other option here | High setup effort |
| A vendor-hosted agent in the cloud | Nothing to install. The vendor runs the isolated machine, and your forge token stays in a proxy outside it, which gives the agent only scoped access to the repository | Your code is copied to the vendor’s machines, and you work with the vendor’s controls instead of your own. It needs a subscription |
| No isolation, permission prompts only | Nothing to set up | The agent has all of your access, and you are the only check |
The vendor’s comparison of these options is the source for every row but the last [2]. A container can also keep a credential in a second container next to it, a sidecar, as the hardened container in the last section does [3].
Whichever you choose, the agent can still change the code you mount for writing, and it can still send what it reads to any host the network lets it reach [2]. A sandbox shrinks the blast radius of a session. What the agent does inside it still needs your review.
Enforce outside the trust boundary
Section titled “Enforce outside the trust boundary”The trust boundary is the line between what the agent decides on its own and what the harness, the operating system or the network enforces on it. An instruction is on the agent’s side of that line. Claude Code loads CLAUDE.md at the start of every session [4], but a line in it is a request, and the model can ignore it or be tricked out of it.
The controls on the other side work in two ways. A permission rule, a hook and your answer at the prompt check the request: Claude Code judges the text of the command before anything runs. The sandbox limits what the running process can reach, and the operating system holds that limit no matter what the command does once it starts [5].
Prompt injection works on the request. Text in a file can convince the agent to ask for a command that your rules allow and that you approve because it looks routine, such as a small script with a harmless name. The check on the request then passes. A limit on the running process never reads that text, so the text can’t change it. That is the rule of this lesson: for the controls that matter most, put limits on what the running process can reach, enforced where nothing the agent reads can change them. A network allowlist and a credential held by a sidecar both work this way:
- A network allowlist enforced by the sandbox or a proxy. The agent can run any command it likes, and a connection to a host that isn’t on the list fails.
- A credential held by a sidecar. The agent’s requests go out through the sidecar, which adds the real token on the way. The agent only ever sees a placeholder, so it has no token to leak [3].
How does each control work?
Section titled “How does each control work?”The lesson separates controls that check an agent's request before it runs from controls that limit what the running process can reach, whatever the command does.
Does this control look at the command before it runs, or at what the process can touch while it runs?
The agent needs a forge token
Section titled “The agent needs a forge token”A coding agent runs in a container on a developer's laptop, and a task needs it to open a pull request, so it needs a token for the code forge.
The agent has finished a change and needs a token for the code forge to open the pull request. Where do you keep the token?
In which of these places does the token itself end up in a tool result or a request to the model?
Set up the built-in sandbox
Section titled “Set up the built-in sandbox”Claude Code’s sandbox covers the shell commands the agent runs and every
program those commands start. The operating system enforces it: Seatbelt on
macOS, and bubblewrap on Linux and WSL2 (the Windows Subsystem for Linux),
where you install bubblewrap and socat first. It doesn’t run on native
Windows [5].
Which files. By default a sandboxed command writes only in the working
folder, a per-user temporary folder and any folder you added. It can
read almost the whole computer, including ~/.ssh and
~/.aws/credentials, until you list those under sandbox.credentials or
sandbox.filesystem.denyRead. Sandboxed commands also inherit the
environment Claude Code was started with, credentials included, unless
sandbox.credentials removes them. Claude Code’s own file tools don’t run
in the sandbox, and permission rules govern them instead. For the .env
case, the Read(./.env) deny rule from the keys lesson blocks the file
tools. Once the sandbox is on, Claude Code adds Read deny rules to the
sandbox’s read blocks too, so a script the agent writes can’t open the
file either [5]. Use denyRead for a path you don’t
want as a permission rule.
Which hosts. Network traffic from sandboxed commands goes through a
proxy that runs outside the sandbox. No host is allowed at the start, and
the first time a command reaches a new host, Claude Code asks you
[5]. allowedDomains lists the hosts that pass
without a question. An entry such as *.example.com covers subdomains, a
:443 suffix limits an entry to one port, and deniedDomains blocks a
host even when an allowed entry covers it [6]. With
strictAllowlist set to true, a host that isn’t on the list is blocked
instead of asked about. Claude Code reads that setting only from your user
settings, managed settings or a file you pass with --settings, and in a
repository’s .claude/settings.json it has no effect. It needs Claude Code
v2.1.219 or later [5].
What happens when the sandbox can’t run a command. If the sandbox can’t
start at all, Claude Code shows a warning and runs commands without it,
unless failIfUnavailable is true, which makes it refuse to start. When
one command fails inside the sandbox, the agent may retry it outside, where
the normal permission prompt applies, and allowUnsandboxedCommands: false
takes that retry away [5]. By default a command that
runs in the sandbox runs without asking you, and
autoAllowBashIfSandboxed: false brings the prompt back
[6].
The course repository has a fixture for this lesson in
site/examples/coding-with-agents/sandboxing/. Its sandbox-settings.json
uses the same setting names as the next lesson’s settings file and allows
one host:
{ "permissions": { "deny": ["WebFetch", "WebSearch"] }, "sandbox": { "enabled": true, "failIfUnavailable": true, "autoAllowBashIfSandboxed": false, "allowUnsandboxedCommands": false, "network": { "allowedDomains": ["pypi.org"], "strictAllowlist": true } }}The allowlist covers the sandboxed commands only. Claude Code’s web fetch
tool runs inside Claude Code itself and follows its permission rules
[5], so the deny rules turn off the web tools as well.
Next to the settings, commands.txt holds three commands that each try to
reach a different host:
curl -sI https://pypi.org/simple/curl -sI https://files.pythonhosted.org/curl -s https://collect.example.invalid/uploadThe last address ends in .invalid, a name reserved so that it never
belongs to a real server [7]. The fixture’s check_allowlist.py makes no
network call. It reads the settings, finds the host and port of each
command and applies the matching rules above.
Which commands get out?
Section titled “Which commands get out?”A Claude Code sandbox settings file allows only pypi.org, with strictAllowlist set to true. Three curl commands try to reach pypi.org, files.pythonhosted.org and collect.example.invalid. A script prints, for each command, whether the sandbox lets its host through.
Read the settings and the three commands above. For each command, decide whether the sandbox lets it through, asks you, or blocks it. Then run the script from the fixture’s folder and compare.
python3 check_allowlist.pyallowedDomains: pypi.org deniedDomains: (none) strictAllowlist: true 1. pypi.org:443 allowed (matches pypi.org) 2. files.pythonhosted.org:443 blocked, no prompt (not on the list) 3. collect.example.invalid:443 blocked, no prompt (not on the list)
Output verified in CI from site/examples/coding-with-agents/sandboxing/check_allowlist.py.
For each host, is it on the list exactly as written, and what does strictAllowlist do with a host that isn't?
Only the first command gets out. The list names hosts, so a host that isn’t written there is blocked, however closely it is related to one that is. When a command you need is blocked, add the one host it needs, and prefer a named host to a wildcard.
Which control stops the upload?
Section titled “Which control stops the upload?”A coding agent runs with a CLAUDE.md that forbids uploads, a deny rule for curl commands, a hook that blocks commands containing curl or wget, and the Claude Code sandbox with a strict network allowlist that lists only pypi.org and unsandboxed retries turned off.
Injected text in a file convinces the agent to upload .env to
paste.example.invalid. It writes a script named sync_check.py that does
the upload and asks to run it, and you approve because the name looks
routine. The sandbox is set so that a blocked command can’t be retried
outside it (allowUnsandboxedCommands: false). Which control still stops
the upload?
Which of these would see the script's connection, whatever the script is called?
What is still exposed?
Section titled “What is still exposed?”A coding agent runs in a container. A sidecar container holds the real forge token and adds it to requests for the forge on their way out, and the agent's environment holds only a placeholder.
Text in a file the agent read has taken over the session. What can it still do with the forge token?
What can the agent do through the sidecar, and for how long?
The fix that keeps holding
Section titled “The fix that keeps holding”After a red-team session, a team found that its coding agent sent project data to an unlisted host, using a small script with a harmless name that the developer approved at the prompt. The team considers four changes.
Which change still holds when the next injected text makes the agent write a different script?
Which of these would still act if the next script had a different name?
Exercise
From the top folder of your clone of the course repository, copy the fixture folder and start Claude Code in the copy with the fixture’s settings, in Manual mode:
cp -R site/examples/coding-with-agents/sandboxing ~/sandbox-trycd ~/sandbox-tryclaude --permission-mode default --settings sandbox-settings.jsonIf ~/sandbox-try is left over from an earlier try, remove it first or
use a new folder name, because cp -R would put the copy inside the old
folder. On the first start in a new folder, Claude Code asks whether you
trust it [8]. Say Yes, since the folder is the copy
you just made. Run /sandbox and check that the sandbox is on. Then ask the agent to run
the commands in commands.txt one at a time and report what each printed.
Approve each command at the prompt, since they only read a web address,
and answer No if the agent asks for anything else. Write down which
commands got a response and which failed with a network error from the
sandbox, and compare that with your prediction. This part runs a real
agent, so the course can’t check its output. The results depend on your
Claude Code version and on your own settings, which Claude Code merges
with the file you pass [9]. A host you allowed there,
or in a WebFetch(domain:...) allow rule, is on the list in this session
too [5]. It takes about five minutes.
A good result is a table with one row per command: the host, what you predicted, what happened, and the setting that decided it. If a row disagrees with your prediction, find the setting from your own files that explains it. Then answer one question: which host would you add for a real project, and what could the agent send there?
Stretch: Run the hardened container from the next section on a copy of the fixture, with no credential flags. Its threat model lists open outbound network, so add an allowlist in front of it, either the built-in sandbox inside the container or a proxy of your own, which then decides which hosts pass, and run the three commands again.
Beyond the built-in sandbox
Section titled “Beyond the built-in sandbox”The summaries below each link to their source, for when the built-in sandbox isn’t enough.
A hardened container with a written threat model. The public
schubergphilis/claude-docker
repository runs Claude Code in a container, and its README says what that
protects and what it doesn’t [3]. Apart from the agent’s own
sign-in, a credential goes in only when you pass its flag. A forge token
stays in a sidecar for the session, and the agent can still use it through
the sidecar while the session runs. The threat model also warns that tools
such as npx and uvx download and run code the first time you use them.
After a compromised session, it says to assume the data has already left
and to rotate every credential that entered the container.
A layered containment pattern. Some teams stack controls from different projects. No single source documents the stack as a whole:
| Layer | What it does | Public building blocks |
|---|---|---|
| Isolation | One container per agent on its own network, with only the workspace mounted | Docker, Podman, dev containers |
| Enforcement outside the agent | A wrapper proxy allows listed hosts and commands, and removes secrets from tool results before they go to the model | mitmproxy, a small proxy of your own |
| Review gate | The agent pushes to a local git server, and a person reviews there before anything reaches the real remote | Forgejo, Gitea, a bare repository |
| Supply-chain hygiene | A secret scan before each commit, and no package version younger than a set age | Gitleaks, pnpm’s minimumReleaseAge |
| Observation | Rules that record network, file and process events around the container without blocking them | Tetragon, Falco |
Gitleaks scans a repository for secrets [10], and pnpm’s
minimumReleaseAge holds back a new package version for a set time, one
day by default [11].
Running with every prompt switched off. In Claude Code that is
--dangerously-skip-permissions, and nothing replaces the prompts it
removes [5]. Anthropic asks that such a session run
where its file tools, MCP servers and hooks are isolated as well, which
means a container, a virtual machine or the sandbox runtime
[2]. Check such a run more closely the less of it you
watched, as the Academy course Claude Code in action advises
[12]. Codex keeps where commands may write
apart from when the agent asks you, and turns the network off by default
[13]. Those are three separate decisions in any agent, and a
setup can be strict on one and loose on another.
The model brings no guardrails. Pi, an open-source coding agent, has no approval step before each tool call. Its documentation leaves safety to whatever limits what Pi can reach, usually a container, a virtual machine or a sandbox around it [14]. The limits come from the harness and the environment, whatever the model.
Recap
- A file the agent reads goes to the model provider in the next request, whether or not a sandbox is on. A sandbox limits which files it can read and which hosts it can send them to [2].
- The ways to contain an agent run from the built-in sandbox, quick to start and limited to shell commands, through the sandbox runtime, containers, dev containers and virtual machines, to a vendor-hosted agent that needs no setup and gives you only the vendor’s controls.
- A permission rule, a hook or a prompt checks what the agent asks for, and injected text can steer that request. For the controls that matter most, limit what the running process can reach: a network allowlist enforced by a proxy, and credentials held by a sidecar.
- In Claude Code,
allowedDomainslists the hosts,strictAllowlistblocks the rest without asking when it comes from your own or a passed settings file, andallowUnsandboxedCommands: falsewithfailIfUnavailable: truestops the agent’s commands from running outside the sandbox [5]. - Keep a token the agent needs in a proxy or sidecar that adds it on the way out, and assume the agent can use it for the whole session.
You can now
- Runs a session from setup to a reviewed diff
References
Section titled “References”- Anthropic. Configure permissions. Claude Code documentation. Reference.
Claude Code permissions - Anthropic. Choose a sandbox environment. Claude Code documentation. Reference.
Claude Code isolation - claude-docker contributors. claude-docker: hardened Docker container for running Claude Code. schubergphilis/claude-docker on GitHub. Reference.
claude-docker - Anthropic. How Claude remembers your project. Claude Code documentation. Reference.
Claude Code memory - Anthropic. Configure the sandboxed Bash tool. Claude Code documentation. Reference.
Claude Code sandboxing - Anthropic. All settings. Claude Code documentation. Reference.
Claude Code all settings - Eastlake and Panitz. Reserved Top Level DNS Names. IETF RFC 2606 (BCP 32). Reference.
RFC 2606 - Anthropic. Security. Claude Code documentation. Reference.
Claude Code security - Anthropic. Settings files and precedence. Claude Code documentation. Reference.
Claude Code settings - Zachary Rice and contributors. Gitleaks. GitHub, gitleaks/gitleaks README. Reference.
Gitleaks - Zoltan Kochan and contributors. Mitigating supply chain attacks. pnpm documentation. Reference.
pnpm supply chain - Anthropic. Claude Code in action. Claude Academy. Course.
Academy claude-code-in-action - OpenAI. Agent approvals & security. Codex documentation. Reference.
Codex approvals - Mario Zechner and contributors. Run Pi safely. earendil-works/pi on GitHub. Reference.
Pi security