# Sandboxing a coding agent

> Compare the ways to contain a coding agent, put the controls that matter outside its trust boundary, and set up Claude Code's built-in sandbox with a network allowlist whose effect you predict.

Page: https://schubergphilis.github.io/ai-training/coding-with-agents/sandboxing/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

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 (Configure permissions, Claude Code documentation), 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
(Choose a sandbox environment, Claude Code documentation). The password has left your machine, and nobody
decided to send it.

[Keeping API keys out of the agent's reach](https://schubergphilis.github.io/ai-training/coding-with-agents/keys-and-secrets/)
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

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 (Choose a sandbox environment, Claude Code documentation). 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 (claude-docker: hardened Docker container for running Claude Code, schubergphilis/claude-docker on GitHub).

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 (Choose a sandbox environment, Claude Code documentation). A sandbox shrinks the blast
radius of a session. What the agent does inside it still needs your review.

## 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 (How Claude remembers your project, Claude Code documentation), 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 (Configure the sandboxed Bash tool, Claude Code documentation).

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 (claude-docker: hardened Docker container for running Claude Code, schubergphilis/claude-docker on GitHub).

#### Checkpoint: How does each control work?

Buckets:

- Checks the request before it runs
- Limits what the running process can reach

Items:

- An ask rule that makes Claude Code prompt before every git push
- A deny rule for Bash(curl *)
- A hook that blocks every command containing the word "secret"
- You approving each command at the permission prompt
- The sandbox's strict network allowlist, enforced by a proxy outside the sandbox
- A forge token the container never holds, kept in a sidecar
- A container with only the project folder mounted

#### Checkpoint: The agent needs a forge token

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?

- Export it as GH_TOKEN in the shell that starts the agent, so the agent's commands can use it.
- Paste it into the chat when the agent asks for it, and delete the message once the pull request is open.
- Keep it in a proxy outside the container that adds it to requests for the forge, while the agent sees a placeholder.
- Put it in the project's CLAUDE.md, with a line that tells the agent never to print it or send it anywhere.

## 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 (Configure the sandboxed Bash tool, Claude Code documentation).

**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 (Configure the sandboxed Bash tool, Claude Code documentation). 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
(Configure the sandboxed Bash tool, Claude Code documentation). `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 (All settings, Claude Code documentation). 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 (Configure the sandboxed Bash tool, Claude Code documentation).

**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 (Configure the sandboxed Bash tool, Claude Code documentation). By default a command that
runs in the sandbox runs without asking you, and
`autoAllowBashIfSandboxed: false` brings the prompt back
(All settings, Claude Code documentation).

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:

```json
{
  "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
(Configure the sandboxed Bash tool, Claude Code documentation), 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:

```text
curl -sI https://pypi.org/simple/
curl -sI https://files.pythonhosted.org/
curl -s https://collect.example.invalid/upload
```

The last address ends in `.invalid`, a name reserved so that it never
belongs to a real server (Reserved Top Level DNS Names, IETF RFC 2606 (BCP 32)). 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.

#### Checkpoint: Which commands get out?

Read the settings and the three commands above. For each command, decide
whether the sandbox lets it through, asks you, or blocks it. The script
first prints `allowedDomains:`, `deniedDomains:` and `strictAllowlist:`,
each followed by its value from the settings, with `(none)` for an empty
list. Then it prints one numbered line per command, such as
`1. host:443`, followed by two spaces and one of
`allowed (matches <entry>)`, `blocked (deniedDomains has <entry>)`,
`asks you (not on the list)` or `blocked, no prompt (not on the list)`.
Write the output, then run the script from the fixture's folder and
compare.

```sh
python3 check_allowlist.py
```

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.

#### Pitfall: The allowlist that let everything out

A developer adds `github.com` to `allowedDomains` so that the agent can push
its branches, and feels safe because the list is short. But anyone can
create an account on that site and receive data there. The sandbox's proxy
decides from the host name alone and doesn't look inside encrypted
traffic, and the vendor warns that a broad domain such as `github.com` can
become a way to send data out (Configure the sandboxed Bash tool, Claude Code documentation). Allow the
narrowest host a command needs. When the agent must reach a broad host,
send its traffic through a proxy of your own that can look inside the
requests, which the same page describes. Your proxy then decides which
hosts pass, so give it the allowlist as well.

## 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:

```sh
cp -R site/examples/coding-with-agents/sandboxing ~/sandbox-try
cd ~/sandbox-try
claude --permission-mode default --settings sandbox-settings.json
```

If `~/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 (Security, Claude Code documentation). 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 (Settings files and precedence, Claude Code documentation). A host you allowed there,
or in a `WebFetch(domain:...)` allow rule, is on the list in this session
too (Configure the sandboxed Bash tool, Claude Code documentation). 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

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`](https://github.com/schubergphilis/claude-docker)
repository runs Claude Code in a container, and its README says what that
protects and what it doesn't (claude-docker: hardened Docker container for running Claude Code, schubergphilis/claude-docker on GitHub). 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 (Gitleaks, GitHub, gitleaks/gitleaks README), and pnpm's
`minimumReleaseAge` holds back a new package version for a set time, one
day by default (Mitigating supply chain attacks, pnpm documentation).

**Running with every prompt switched off.** In Claude Code that is
`--dangerously-skip-permissions`, and nothing replaces the prompts it
removes (Configure the sandboxed Bash tool, Claude Code documentation). 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
(Choose a sandbox environment, Claude Code documentation). Check such a run more closely the less of it you
watched, as the Academy course *Claude Code in action* advises
(Claude Code in action, Claude Academy). Codex keeps where commands may write
apart from when the agent asks you, and turns the network off by default
(Agent approvals & security, Codex documentation). 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 (Run Pi safely, earendil-works/pi on GitHub). The limits come from the harness
and the environment, whatever the model.

## Recap

1. 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 (Choose a sandbox environment, Claude Code documentation).
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.
3. 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.
4. In Claude Code, `allowedDomains` lists the hosts, `strictAllowlist` blocks the rest without asking when it comes from your own or a passed settings file, and `allowUnsandboxedCommands: false` with `failIfUnavailable: true` stops the agent's commands from running outside the sandbox (Configure the sandboxed Bash tool, Claude Code documentation).
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.

#### Habit

Before you let an agent run commands in a new project, write down the hosts those commands really need, and check that your allowlist names those hosts and nothing wider.

## References

- Configure permissions, Claude Code documentation, https://code.claude.com/docs/en/permissions
- Choose a sandbox environment, Claude Code documentation, https://code.claude.com/docs/en/sandbox-environments
- claude-docker: hardened Docker container for running Claude Code, schubergphilis/claude-docker on GitHub, https://github.com/schubergphilis/claude-docker
- How Claude remembers your project, Claude Code documentation, https://code.claude.com/docs/en/memory
- Configure the sandboxed Bash tool, Claude Code documentation, https://code.claude.com/docs/en/sandboxing
- All settings, Claude Code documentation, https://code.claude.com/docs/en/settings-reference
- Reserved Top Level DNS Names, IETF RFC 2606 (BCP 32), https://www.rfc-editor.org/rfc/rfc2606
- Security, Claude Code documentation, https://code.claude.com/docs/en/security
- Settings files and precedence, Claude Code documentation, https://code.claude.com/docs/en/settings
- Gitleaks, GitHub, gitleaks/gitleaks README, https://github.com/gitleaks/gitleaks
- Mitigating supply chain attacks, pnpm documentation, https://pnpm.io/supply-chain-security
- Claude Code in action, Claude Academy, https://academy.claude.com/courses/claude-code-in-action
- Agent approvals & security, Codex documentation, https://learn.chatgpt.com/docs/agent-approvals-security
- Run Pi safely, earendil-works/pi on GitHub, https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/security.md
