Skip to content

One File That Locks Down an AI Agent (Tested Live)

Telling an AI agent it has no internet is not a control. One Claude Code sandbox file gives it exactly one allowed domain and zero readable keys — tested live, with the four gotchas nobody documents.

Anthropic reviewed 141,006 evaluation runs and found three where a model reached the real systems of outside companies. Their own conclusion was not “the AI went rogue” — it was that this sat “closer to a harness and operational failure than a model alignment failure.”

The evaluation prompt told the model it had no internet access. Nothing enforced it.

That distinction is the whole point of this post. A sentence in a prompt is not a security control. Below is the file that turns it into one, and the result of actually running it on my machine.

The result first

The lockdown run — 200, 000, and Operation not permitted

Three jobs handed to the agent, one command:

  • Reach the package registry → 200
  • Reach any other site → 000 (the connection never happened)
  • Read ~/.sshOperation not permitted

Not a model politely declining. The operating system refusing.

The run, live

The file

The config, annotated

{
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "autoAllowBashIfSandboxed": true,
    "network": {
      "strictAllowlist": true,
      "allowedDomains": ["registry.npmjs.org"]
    },
    "credentials": {
      "files": [
        { "path": "~/.aws/credentials", "mode": "deny" },
        { "path": "~/.ssh", "mode": "deny" }
      ]
    }
  }
}

Save it as lockdown.json and run:

claude --settings ./lockdown.json -p "<your task>"

Start with allowedDomains empty. Add a domain only when something you actually trust fails. Building the allowlist from real failures is faster than guessing it up front — and you end up with a list you can justify.

The four things people get wrong

1. strictAllowlist is ignored from your repo. It is honoured only from user settings, managed settings, or the --settings flag. Put it in a project’s .claude/settings.json and it silently does nothing — which is worse than not setting it, because you believe you are protected.

2. Without allowUnsandboxedCommands: false, the agent can step outside. When a command fails under the sandbox, Claude Code may retry it unsandboxed. I hit this while testing: the blocked request came back 200 because the agent had simply run it again outside the boundary. One line closes it.

3. Your token may never be used at all. This is the one that wastes afternoons:

Credential precedence — why your token is ignored

ANTHROPIC_API_KEY sits at position 3. CLAUDE_CODE_OAUTH_TOKEN sits at position 5. One stale environment variable on one machine and your one-year token is never consulted — which looks exactly like “the token doesn’t work on that server.”

/status   # tells you which credential actually won

4. Platform support is not uniform. macOS enforces with Seatbelt and needs nothing installed. Linux and WSL2 need bubblewrap and socat. Native Windows is not supported — run it inside WSL2.

When to do this, and when not to

The trade-off

The honest limits: exploratory work fights the allowlist, installers and scrapers need many hosts, and --bare scripts ignore CLAUDE_CODE_OAUTH_TOKEN entirely. Sandboxing also is not a complete isolation boundary — the built-in proxy does not inspect TLS by default, so a broad allowedDomains entry is still a path out. Narrow lists are the point.

The takeaway

The incident that started this was not exotic. Nobody exploited a zero-day. A prompt said “no internet”, a misconfiguration meant there was internet, and the model did what it was asked to do with the access it actually had.

Your agent is in the same position right now. The difference between “I told it not to” and “it cannot” is one file.

Ready to Build Real Cloud AI Solutions?

Stop just reading tutorials. Build real, deployable AI cloud solutions on AWS, Azure, and GCP platforms. Get hands-on with production-grade projects that prove you can ship—not just study.


Every number in this post came from running these commands on one machine on 1 August 2026, on Claude Code v2.1.220. Behaviour changes between versions — check /status and your own output rather than trusting a blog post, including this one.