Learn

How AI Coding Agents Leak Secrets, and What Actually Stops It

An agent does not need a zero-day to leak a credential. It reads a file it was allowed to read, then makes a request it was allowed to make. Both halves are approved, and the combination is the breach. What each defence layer catches, and where each one stops.

8 min readIn depth

A coding agent with shell access inherits every secret your shell can reach. Not through a vulnerability. By design, because that is what makes it able to run your tests and deploy your branch.

The leak, when it happens, is made of two ordinary actions:

  1. It reads a credential file. Reading files is the job.
  2. It makes an outbound request. Making requests is also the job.

Neither half is suspicious. The breach is the sequence, and nothing in a per-action approval model can see a sequence.

What "reachable" means, concretely

Before defending anything, it is worth knowing what is actually exposed. On a typical developer machine an agent with your shell can reach, without one unusual command:

WhereWhat is in it
~/.aws/credentialslong-lived cloud keys, often with broad IAM
~/.ssh/id_*the key that authenticates you to every host
~/.config/gh/hosts.ymla GitHub token, often with repo and workflow
~/.npmrc, ~/.pypircpublish tokens for your packages
.env in any project you openeddatabase URLs with the password inline
~/.kube/configcluster admin, frequently

None of these need privilege escalation. They are files in your home directory and the agent runs as you.

The four ways it gets out

Straight read and post. cat a credential, curl it somewhere. The crude version, and the one every tool catches.

Through an approved destination. The same secret, sent to a host already on your allowlist. A POST to your own GitHub repository carrying .env in the body passes a domain allowlist cleanly, because the destination was never the problem.

Encoded or split. base64 prints the same bytes as cat. A value split across two requests reassembles at the other end. Anything that matches on the literal text of a command loses here, because the attacker writes the text.

Quietly, inside something legitimate. A secret pasted into a commit message, a bug report the agent files for you, or a stack trace it uploads for help. No exfiltration tool was involved, and the value is gone all the same.

What each layer of defence actually covers

The important thing is that these are not alternatives. Each one is blind to what the others see.

LayerStopsBlind to
Permission promptsactions you did not approveanything you did approve
Command allowlistsforbidden verbsthe file an allowed verb opens
Egress / domain allowlistsunknown destinationsthe payload, and any allowed host
Content scanning (DLP)recognised secret shapes in what leavesa secret with no recognisable shape
Credential jailreads of known credential pathsa secret that lives somewhere unusual

Two conclusions follow. A domain allowlist alone is not secret protection, because it has no opinion about a request body. And content scanning alone is not either, because it can only match shapes somebody thought to describe.

The defence that holds up

Stop the read, not just the send. Once a credential is in the agent's context it has already been sent to a model provider, and every later control is damage limitation.

That means a rule about files, evaluated on the structure of the command rather than its text. It has to survive a different verb, a wrapper, and a different tool reaching the same path.

This is what node9 does at the point the agent acts, and these are real verdicts from the live gate, with a control so the result means something:

cat README.md                 →  ✅ ALLOW    control: an ordinary file
cat ~/.ssh/id_rsa             →  🛑 BLOCK    project-jail, resolved from the AST
base64 ~/.ssh/id_rsa          →  🛑 BLOCK    different verb, same file, same answer
Read{file_path: ~/.ssh/id_rsa} →  🛑 BLOCK   the agent's own file tool, same rule

The last line is the one people miss. A credential can be opened through the shell or through the agent's built-in Read, Grep, and Glob tools. Those are two separate code paths inside any gate, and a rule wired to one of them leaves the other open.

Alongside that, two things run without configuration:

  • Fifty-eight named provider credential shapes are matched in every tool argument, so a key pasted into a command is refused whether or not its file was jailed.
  • The cloud metadata address is permanently unreachable. Verified on both doors: a shell curl to 169.254.169.254 and a WebFetch to the same address are both refused, and the refusal says the address cannot be allowlisted at all. That address is how a stolen role becomes stolen cloud credentials.

Where this stops

  • Egress gates destinations, not payloads. Deciding which hosts an agent may reach is a different control from deciding what may be in the request. You want both.
  • A secret with no shape is not detectable as a secret. An internal token that looks like a random word is a string until something tells the scanner otherwise.
  • Commands that build a path at runtime are read less reliably than a literal one. Structural parsing is much harder to fool than text matching, and it is not a proof.
  • Attachments bypass tool gating entirely. In Claude Code, a file attached with @ never becomes a tool call, so no hook sees it, in any product of this kind.

Check your own machine

npx node9-ai posture     # what is reachable here, about 60 seconds, no install
npx node9-ai scan        # what your agents already did

Then rotate whatever those two turn up before you harden anything. A credential an agent has already read is a credential that has already left the building.