How I Contain Coding Agents: Two of Three, Never All Three

Conceptual illustration · not a product screenshot

An agent becomes dangerous when one session has three things at once: your private data, text written by a stranger, and a way to send something out. Give it all three and one poisoned issue, web page or file can carry your data out the door. Remove any one and that path closes. An injection can still try to steer the work itself, which is what the later rules here are for.

Simon Willison named this combination the lethal trifecta in June 2025. It's the rule I build my agent setup around, and the reason is simple: I can't stop an agent from reading hostile text. It reads issues, pull request comments, CI logs, scraped pages and instruction files all day. So I take away one of the other two.

These are my own notes, from running coding agents on ezeo.ai, jamaklab.com and this site. They describe my setup, not a standard, and I don't have attack numbers to show you.

Why this got more urgent this autumn

Coding agents now read instruction files in their working directory by design. Claude Code, for example, loads CLAUDE.md files as project memory. That's useful, and it also means a file the agent didn't write can steer it. At the end of October, researchers reported bugs in five AI developer tools, some still unpatched when the story ran (The Register, October 29, 2026).

I don't wait for each vendor fix. I assume the agent will, sooner or later, read an instruction I didn't write.

Rule 1: no secrets in the box

My agents run in cloud containers, not on my laptop. The container doesn't hold production database passwords or API keys for the services that matter. When an agent needs Supabase, Vercel or Stripe, it goes through a connector that acts for me with its own permissions, and the writes are visible in the session.

Secrets live upstream, in 1Password and in each platform's own secret store, and never in a .env file that an agent can cat. If an agent is tricked into printing everything it can see, the worst it prints is a branch name.

Rule 2: no open network

The container's outbound traffic goes through a proxy with an allowlist. Today it refuses Railway, Supabase, Vercel, Sentry, PostHog and Stripe directly, and most sites I haven't added. That annoys me weekly: while writing this post, the agent couldn't fetch the very articles it was citing.

It's still the right trade. An injected instruction that says "send this to my server" fails at the network, not at the model's judgement. Cutting the way out is the cheapest of the three to cut, because I can still read the outside world through search and decide what comes in.

It doesn't close every way out. The allowlist blocks direct sends to arbitrary hosts, but a pull request is also an outbound channel: data an agent read through a connector could be written into a branch. Rule 1 keeps secrets out of that, since there are none in the box to copy. For connector data, the pull request is a narrower channel than the open internet, not a closed one, and it's the next one I want to tighten.

Three white panels under the headline "Two of three, never all three.": Private data, secrets, production, inbox; Untrusted input, issues, web pages, files; and, marked with the yellow square, A way out, network, pushes, email. Bottom line: "My default: cut the way out."
An agent can have two of these safely. My default is to remove the third: the way out.

Rule 3: the only way out is a pull request

An agent's work leaves the box one way: a branch and a pull request. Agents never commit to main, and each task runs in its own git worktree, because on 2026-08-02 one session's reset wiped another session's uncommitted work in a shared checkout. No warning, no conflict.

On my repos, merging deploys, so the pull request is the gate that matters. My agents may merge their own PR when every required check is green. That permission is written down, with a list of cases where they must stop and ask me. The permission layer also refuses history rewrites: when an agent recently tried to force-push, the push was blocked, and it found another way that kept history intact.

Rule 4: gates that fail loudly

A check that can pass while checking nothing is worse than no check, because it tells the agent and me that all is well. My TypeScript gate once printed "no new errors" while it was compiling zero files. It now refuses to pass unless it actually compiled the source tree, and the agent instructions say not to touch those guards.

The same goes for CI. My builds run on a self-hosted runner, and I only attach private repositories to it. A public repo would let anyone's pull request run code on that machine.

Rule 5: instruction files are code

AGENTS.md, CLAUDE.md and the rule files under .claude/ live in the repository and change through pull requests like any other code. A test caps their size, so they stay readable enough for a person to review. Text that arrives from outside, such as an issue, a comment, a CI log or a fetched page, is data. The agent harness I use labels it that way, and I check the transcript when something looks off.

What I'd do first on a new project

  1. List what the agent can reach: data, input sources, outbound channels.
  2. If all three are there, remove one, starting with the network.
  3. Take production secrets out of the agent's environment.
  4. Make a pull request the only way changes ship, with required checks.
  5. Make each gate prove it ran, not just that it passed.

If you're shipping an AI feature to users rather than running agents on your own code, the deployment readiness checklist covers the product side. And if you're setting up an agent workspace for the first time, start with how I use Claude Code projects.

FAQ

What is the lethal trifecta for AI agents?

Simon Willison's name for an agent that combines access to private data, exposure to untrusted content, and a way to communicate externally. With all three, a prompt injection in the untrusted content can make the agent send the private data out. Removing any one of the three closes that exfiltration path, though an injection can still try to steer the agent's other actions.

How do I stop a coding agent from leaking secrets?

Don't give it secrets. Keep production credentials out of the agent's environment, use scoped connectors for the services it needs, and restrict its outbound network to an allowlist, so an injected "send this to my server" fails at the network.

Should a coding agent be allowed to merge its own pull requests?

Only behind required checks that actually prove something, and only with written rules about when it must stop and ask. On my repos, merging deploys, so I treat the merge permission as a production permission.

(05) CONTACT

What are youworking on?

A product to build. A system to deploy. A decision to work through.

Start a conversation