Run Claude Code and Cursor's CLI agent inside a hardened, egress-filtered container with a human approval gate.
What you'll build / learn #
A hardened Docker container that runs Claude Code and the Cursor CLI in full-autonomy mode β with dropped Linux capabilities, CPU/memory/PID limits, an iptables egress allowlist so the agent can only reach the APIs it needs, and a host-side approval gate for anything privileged.
flowchart LR
subgraph host [Host]
W[Your repo] -- bind mount --> C
A[approve.sh] -- "docker exec (after y/N)" --> C
end
subgraph C [Locked-down container]
CC[claude / agent as non-root user]
end
C -- "allowlisted domains only" --> API[api.anthropic.com, api2.cursor.sh, GitHub, npm]
C -. everything else .-x X[blocked by iptables]
Prerequisites #
Verified August 2026 against: Docker Engine 29.7 (any recent 27+ works), Claude Code 2.1.237, the Cursor CLI 2026.08 build, and the node:22-bookworm
image (Claude Code requires Node.js β₯ 22).
- Docker Engine or Docker Desktop on Linux or macOS (Windows: use WSL2).
- A Claude subscription or Anthropic Console account. Run
claude setup-token
on your host and copy the long-lived token it prints, or grab an API key from the Console. - Optional, for Cursor: an API key from cursor.com β Dashboard β API Keys.
1. Vendor the egress firewall script #
Anthropic ships a reference firewall for exactly this purpose. It sets iptables
default policies to DROP
, resolves an allowlist of domains into an ipset
, and self-tests on startup. Download it into a fresh project directory:
mkdir agent-sandbox && cd agent-sandbox
curl -fsSL -o init-firewall.sh \
https://raw.githubusercontent.com/anthropics/claude-code/main/.devcontainer/init-firewall.sh
Out of the box it allows the Anthropic API, GitHub, and npm. The ipset holds resolved IPs, not wildcards, so if you'll run Cursor's agent, add its backend domains (documented here) to the for domain in
loop in the script:
"api2.cursor.sh" \
"api3.cursor.sh" \
"repo42.cursor.sh" \
"authenticator.cursor.sh" \
"downloads.cursor.com" \
2. Write the entrypoint #
The firewall needs root and NET_ADMIN
, but the agent must never have either. So the container starts as root, raises the firewall, then drops to an unprivileged user with gosu
β no sudo
installed, so there's no way back up. Save as entrypoint.sh
:
#!/bin/bash
set -euo pipefail
/usr/local/bin/init-firewall.sh
exec gosu node "$@"
3. Write the Dockerfile #
FROM node:22-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
iptables ipset dnsutils jq curl ca-certificates git gosu aggregate \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code
COPY init-firewall.sh entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh \
&& mkdir -p /home/node/.claude /workspace \
&& chown -R node:node /home/node /workspace
USER node
RUN curl https://cursor.com/install -fsS | bash
ENV PATH="/home/node/.local/bin:${PATH}"
USER root
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["bash"]
Build it:
docker build -t agent-sandbox .
4. Run it locked down #
Export credentials on the host (never bake them into the image), then start the container from your project's directory. Save as run-sandbox.sh
next to the Dockerfile:
#!/usr/bin/env bash
set -euo pipefail
docker run --rm -it --name agent-sandbox \
--cap-drop=ALL \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--cap-add=SETUID --cap-add=SETGID \
--security-opt no-new-privileges:true \
--memory=4g --memory-swap=4g --cpus=2 --pids-limit=512 \
-v "$PWD":/workspace \
-v claude-sandbox-config:/home/node/.claude \
-e CLAUDE_CONFIG_DIR=/home/node/.claude \
-e CLAUDE_CODE_OAUTH_TOKEN \
-e CURSOR_API_KEY \
-w /workspace \
agent-sandbox
What each layer buys you: --cap-drop=ALL
strips every capability except the four the entrypoint needs (NET_ADMIN
/NET_RAW
for iptables, SETUID
/SETGID
for the gosu drop); no-new-privileges
blocks re-escalation via setuid binaries; --memory-swap
equal to --memory
disables swap so a runaway build gets OOM-killed instead of thrashing your host; --pids-limit
caps fork bombs. The only host surface is the bind-mounted repo. Don't mount ~/.ssh
or cloud credential files β anything visible in the container is visible to the agent.
Inside, run the agents at full autonomy β the container is the guardrail:
claude --dangerously-skip-permissions
agent -p --force "add input validation to server.js"
5. Add the approval-gated exec bridge #
The agent runs as node
with no sudo, so it can't install system packages or touch iptables. When it needs something privileged β say apt-get install ripgrep
β it has to ask, and you approve from the host. Save as approve.sh
on the host and chmod +x
it:
#!/usr/bin/env bash
set -euo pipefail
echo "Sandbox requests: $*"
read -rp "Run as root inside the container? [y/N] " reply
if [[ "$reply" == "y" ]]; then
docker exec --privileged -u root -it agent-sandbox "$@"
else
echo "Denied."
fi
Usage, while the sandbox is running:
./approve.sh bash -c 'apt-get update && apt-get install -y ripgrep'
docker exec --privileged
grants full capabilities to that one approved command only; the agent's own shell stays stripped.
Verify it works #
Startup should end with the firewall's self-test before your prompt appears:
Firewall verification passed - unable to reach https://example.com as expected
Firewall verification passed - able to reach https://api.github.com as expected
node@2f8c1a9b3d47:/workspace$
Confirm the lockdown from inside the container:
curl -m 5 -sI https://example.com || echo BLOCKED # prints BLOCKED after 5s
curl -s https://api.github.com/zen # prints a zen aphorism
claude --version # 2.1.237 (Claude Code)
agent --version # prints the Cursor CLI build
claude -p "reply with exactly: sandbox ok" # sandbox ok
And the resource caps from the host:
docker inspect -f '{{.HostConfig.Memory}} {{.HostConfig.PidsLimit}}' agent-sandbox
Troubleshooting #
at startup β the container is missing network capabilities. You droppediptables v1.8.9 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)
ALL
without adding backNET_ADMIN
andNET_RAW
(easy to do when porting thedocker run
flags to Compose: usecap_add
).β you swapped in a base image without the/usr/local/bin/init-firewall.sh: line 30: aggregate: command not found
aggregate
package, which the script uses to merge GitHub's CIDR ranges. Addaggregate
to theapt-get install
line and rebuild.β you're running--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons
claude
as root, probably from adocker exec -u root
shell. Claude Code refuses bypass mode as root by design; run it from the main shell, where the entrypoint already dropped tonode
.(macOS) β your project directory is outside Docker Desktop's allowed shares. Add it under Settings β Resources β File Sharing.Error response from daemon: Mounts denied: The path ... is not shared from the host
Next steps #
- Anthropic's
reference devcontainerwraps this same Dockerfile-plus-firewall pattern in the Dev Containers spec, so VS Code, Cursor, and JetBrains can attach an editor to the sandbox. - Read Claude Code's
security modelandsandbox environmentscomparison β the built-in Bash sandbox may be enough when you don't need full container isolation. - Pin versions for reproducibility: install
@anthropic-ai/claude-code@2.1.237
explicitly and setDISABLE_AUTOUPDATER=1
incontainerEnv
so the agent can't update itself mid-session. - Harden further with a read-only root filesystem (
--read-only
plus tmpfs mounts) or run the whole thing rootless withRootless Docker.
Sources & further reading #
Development containers - Claude Code Docsβ code.claude.com - init-firewall.sh reference egress firewallβ github.com - Claude Code reference devcontainer configurationβ github.com - Using Headless CLI - Cursor Docsβ cursor.com - Network Configuration - Cursor Docsβ cursor.com - Docker Engine version 29 release notesβ docs.docker.com
Mariana SouzaΒ· Senior Editor
Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.
Discussion 0 #
No comments yet
Be the first to weigh in.