# Sandbox Your AI Coding Agent in a Locked-Down Docker Container

> Source: <https://sourcefeed.dev/a/sandbox-your-ai-coding-agent-in-a-locked-down-docker-container>
> Published: 2026-08-20 11:42:02+00:00

# Sandbox Your AI Coding Agent in a Locked-Down Docker Container

Run Claude Code and Cursor's CLI agent inside a hardened, egress-filtered container with a human approval gate.

[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)

## What you'll build / learn

A hardened Docker container that runs [Claude Code](https://code.claude.com/docs/en/overview) and the [Cursor CLI](https://cursor.com/docs/cli/overview) 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.

``` php
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](https://cursor.com/docs/enterprise/network-configuration)) 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`

:

``` bash
#!/bin/bash
set -euo pipefail
/usr/local/bin/init-firewall.sh
exec gosu node "$@"
```

## 3. Write the Dockerfile

```
FROM node:22-bookworm

# aggregate, ipset, dnsutils, jq: required by init-firewall.sh
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

# Cursor CLI installs to ~/.local/bin for the invoking user
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:

``` bash
#!/usr/bin/env bash
set -euo pipefail
# export CLAUDE_CODE_OAUTH_TOKEN=...   (from `claude setup-token`)
# export CURSOR_API_KEY=...            (optional)
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
# or Cursor, headless:
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:

``` bash
#!/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
# 4294967296 512
```

## Troubleshooting

at startup — the container is missing network capabilities. You dropped`iptables v1.8.9 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)`

`ALL`

without adding back`NET_ADMIN`

and`NET_RAW`

(easy to do when porting the`docker run`

flags to Compose: use`cap_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. Add`aggregate`

to the`apt-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 a`docker exec -u root`

shell. Claude Code refuses bypass mode as root by design; run it from the main shell, where the entrypoint already dropped to`node`

.(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 devcontainer](https://code.claude.com/docs/en/devcontainer)wraps 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 model](https://code.claude.com/docs/en/security)and[sandbox environments](https://code.claude.com/docs/en/sandbox-environments)comparison — 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 set`DISABLE_AUTOUPDATER=1`

in`containerEnv`

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 with[Rootless Docker](https://docs.docker.com/engine/security/rootless/).

## Sources & further reading

-
[Development containers - Claude Code Docs](https://code.claude.com/docs/en/devcontainer)— code.claude.com -
[init-firewall.sh reference egress firewall](https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh)— github.com -
[Claude Code reference devcontainer configuration](https://github.com/anthropics/claude-code/blob/main/.devcontainer/devcontainer.json)— github.com -
[Using Headless CLI - Cursor Docs](https://cursor.com/docs/cli/headless)— cursor.com -
[Network Configuration - Cursor Docs](https://cursor.com/docs/enterprise/network-configuration)— cursor.com -
[Docker Engine version 29 release notes](https://docs.docker.com/engine/release-notes/29/)— docs.docker.com

[Mariana Souza](https://sourcefeed.dev/u/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.
