Secure deployment
Secure deployment is the set of boundaries you put around an agent that can run code, read files and call out to the network. The agent decides what to do as it goes, so the boundaries, rather than its code path, are what bound the damage.
Last updated: 20 Oct, 2026 · Claude Agent SDK
An agent's actions are shaped by the content it reads. A file, a web page or a message can carry instructions, and the agent may act on them in ways nobody intended, which is the risk known as prompt injection. Claude models are trained to resist it, and defence in depth is still the practice: if an agent is talked into sending data somewhere, a network control can refuse the request outright. The secure deployment guide is the source for this lesson.
Not every deployment needs all of this. One person running an agent on their own laptop has a different problem from a service handling other people's data. Read it as a ladder and climb as far as your situation warrants.
What Claude Code brings already
- Permissions. Every tool and every shell command can be allowed, blocked, or put to a person, with glob patterns for rules such as allowing one command family.
- Command parsing. A shell command is parsed into a syntax tree and matched against your rules before it runs. One that does not parse cleanly, or does not match an allow rule, needs approval, and a few constructs always do.
- Summarised web search. Search results are summarised rather than pasted into the context, which narrows what a hostile page can say to the agent.
- Sandbox mode. Shell commands can run with the filesystem and the network restricted.
The command parsing is a permission gate and not a sandbox. Beyond a few built-in safety checks it does not work out from a command's target whether that command is dangerous. Treat it as the thing that applies your rules, and put the isolation underneath it.
Turning the sandbox on from the options
The sandbox option is a settings dictionary the SDK forwards to Claude Code, which is the side that enforces it. On this install the forwarding is visible in the command line, so the run below shows the settings arriving even though nothing sandboxed is executed here.
import shlex
from claude_agent_sdk import ClaudeAgentOptions
from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport
# failIfUnavailable stops the run rather than falling back to an unsandboxed shell
box = {"enabled": True, "failIfUnavailable": True,
"network": {"allowedDomains": ["api.github.com"]}}
# the settings are forwarded whole; Claude Code is what applies them
options = ClaudeAgentOptions(sandbox=box, allowed_tools=["Bash"], cli_path="claude")
started = SubprocessCLITransport(prompt="", options=options)._build_command()
print(shlex.join(started[1:]))--output-format stream-json --verbose --system-prompt '' --allowedTools Bash --settings '{"sandbox": {"enabled": true, "failIfUnavailable": true, "network": {"allowedDomains": ["api.github.com"]}}}' --input-format stream-jsonReading the settings flag
- The whole dictionary went across as
--settings. Fields the Python type does not declare,failIfUnavailableandfilesystemamong them, are forwarded with the rest. failIfUnavailablematters on Python. The documented Python default is to carry on unsandboxed with a warning when the sandbox cannot start, which differs from the TypeScript SDK. Setting it makes the run stop instead, with an error result you can read.- The allowed domains bound the network. Sandboxed commands reach the names on the list and no others, and a denied list wins over the allowed one.
- Nothing is enforced here. This run starts no process and executes no shell command. A real sandbox needs platform support, and on Linux tools such as
bubblewrap.
The sandbox proxy matches on the hostname a client asks for and does not decrypt the traffic, so a determined program inside the sandbox can reach a host that is not on the list. Where that matters, terminate TLS at a proxy of your own.
Choosing how strong the isolation has to be
| Technology | Isolation | Overhead | Setup |
|---|---|---|---|
| Sandbox runtime | Good, with sensible defaults | Very low | Low: no container or networking work |
| Containers | Depends entirely on the configuration | Low | Medium |
| gVisor | Excellent when set up correctly | Medium to high | Medium |
| Virtual machines | Excellent when set up correctly | High | Medium to high |
Sandbox runtime is the low-effort step up, using the operating system's own primitives for the filesystem and routing the network through a built-in proxy. Its limit is that the sandboxed processes share your kernel. gVisor answers that by handling most system calls in userspace before they reach the real kernel, at the cost of slower file-heavy work. Virtual machines give each agent its own kernel, and a microVM can boot in a fraction of a second.
Hardening the container the agent runs in
Where a container is the boundary, the flags are what make it one. This shape drops every Linux capability, forbids gaining privileges, keeps the root filesystem immutable with one small writable scratch area, removes the network outright, and mounts the code read-only.
# no capabilities, nothing writable but one scratch mount, and no network at all
docker run --cap-drop ALL --security-opt no-new-privileges --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=100m --network none \
--memory 2g --pids-limit 100 --user 1000:1000 \
-v /path/to/code:/workspace:ro \
-v /var/run/proxy.sock:/var/run/proxy.sock:ro \
agent-imageWith no network interfaces, the only way out is the Unix socket mounted at the end, which connects to a proxy running outside the container. The proxy is where domain allowlists are enforced, credentials are added and traffic is logged. An agent that has been talked into exfiltrating data cannot reach an arbitrary server, because there is no route to one.
Keeping the credential outside the agent
The strongest pattern for secrets is to not put them where the agent is. A proxy outside the boundary adds the credential to each outgoing request: the agent makes the call, the proxy makes it authenticated. The agent never holds the secret, the proxy enforces which endpoints are reachable, and every request is logged in one place.
# sampling requests only, in plaintext, so the proxy can add the header itself
export ANTHROPIC_BASE_URL="http://proxy.internal:8080"
# everything the process speaks, as an opaque tunnel unless the proxy terminates TLS
export HTTP_PROXY="http://proxy.internal:8080"
export HTTPS_PROXY="http://proxy.internal:8080"The two differ in what the proxy can see. Requests to the Claude API sent through the base URL arrive in plaintext, so a header can be added. Ordinary HTTPS to other services arrives as an encrypted tunnel, and adding anything to it means a TLS-terminating proxy with its certificate installed in the agent's trust store. An MCP server that makes the authenticated call outside the boundary avoids that work: the agent sees the tool, not the secret.
Never put a real key into a lesson file, a repository or an image layer. Read it from a secret manager at start, or route the call through the proxy so the agent's environment has no key to leak.
Mounting code read-only, and what to keep out
Mounting a directory read-only stops the agent writing to it. It does not stop the agent reading a secret that is sitting in it, and a working tree usually has several. Copy in the source the task needs, or filter the mount.
.envand.env.local, which hold API keys and database passwords.~/.aws/credentials,~/.config/gcloud/and~/.azure/, which hold cloud access.~/.git-credentialsand~/.docker/config.json, which hold tokens in plain text.~/.kube/config,.npmrc,.pypirc, and any*.pemor*.keyfile.
Where the agent has to write, a memory-backed scratch mount is cleared when the container stops. Where the writes are meant to survive, mount a volume for them and keep it away from anything sensitive.
The sandbox option against a hardened container
sandbox option | Hardened container | |
|---|---|---|
| What it bounds | Shell commands the agent runs | The whole process, the SDK included |
| Where it is set | In your Python, per call | In the deployment, outside the code |
| Network control | Domain lists, matched on the requested hostname | No interface at all, with a proxy on a socket |
| Who enforces it | Claude Code, using the operating system's primitives | The container runtime and the kernel |
How far up the ladder to climb
- One developer on one machine: the permissions system and sandbox mode are a reasonable stopping point.
- An agent reading content from outside, such as web pages or customer attachments: add the container and the egress proxy.
- Many tenants on shared infrastructure: add per-tenant isolation, per-tenant egress rules, and kernel-level isolation if the content is untrusted.
Related
- Previous: Hosting the SDK
- Next: The project and its tools
- See also: Permission modes
- Reference: Securely deploying AI agents
- Add
deniedDomainsbeside the allowed list and read the settings flag again. - Drop
failIfUnavailableand say which of the two behaviours you would want in a service. - Add
"filesystem": {"denyWrite": ["/work/.env"]}to the dictionary and confirm it is forwarded.
You understood something today that you didn't yesterday.