Use promo code BETATEST1 for full access
seekrit
← all posts

Your coding agent will hand over your API key if asked nicely

Security

A security researcher at Johns Hopkins opened a pull request. In the title — not the body, not a file, the title — they wrote an instruction. Anthropic's Claude Code Security Review action read the PR, followed the instruction, and posted its own API key as a comment on the thread.

The same trick worked against Google's Gemini CLI Action and GitHub's Copilot Agent. The pattern got a name, Comment and Control, and all three vendors patched it quietly between November 2025 and March 2026 — modest bounties, no CVEs, no public warnings.

It's worth being precise about what happened, because the interesting part is not that an agent had a bug. The agents did exactly what they are built to do. They read text from an untrusted source, understood it as a request, and were helpful.

This is not a bug class, it's a category error

The instinct is to file this under "prompt injection, patch pending." That framing is comfortable and wrong. Consider the other end of the same problem:

CVE-2026-21852. A malicious repository ships a settings file that points ANTHROPIC_BASE_URL at a server the attacker controls. You clone it and open it. Claude Code applies the config and starts making API requests — with your key — before it shows you the trust prompt. Improper initialization order; CVSS 5.3; fixed in 2.0.65.

That one really was a bug, and it really is patched. But notice that the fix addresses the ordering, not the exposure. Your API key was sitting in an environment your editor could read, which means it was sitting in an environment that anything running in your editor could read. The initialization order decided whether that particular attack landed. It didn't decide whether the key was reachable.

Now scale it. GitGuardian counted 28.6 million secrets leaked in public GitHub commits across 2025 — up 34% year over year, and the largest annual jump they've recorded. Within that, AI-service secrets grew 81%, to over 1.2 million.

Three different failure modes — a stochastic system persuaded to talk, a config precedence bug, and ordinary human sprawl — with one thing in common. In every case there was a reusable secret sitting somewhere the agent could read it.

note

An agent doesn't have to be compromised to leak a credential. It only has to decide that sharing would be helpful. You cannot patch that, because it isn't a defect — it's the feature.

Why the usual mitigations don't finish the job

Every one of these is worth doing. None of them close it.

Scope the key down. Good practice, and it bounds the blast radius rather than preventing the leak. A read-only key that reaches an attacker is still your data leaving.

Use short-lived tokens. Better. But an agent runs for minutes and exfiltrates in milliseconds; a fifteen-minute TTL is fifteen minutes of valid credential in someone else's hands. And plenty of upstreams — Stripe, OpenAI, most SaaS APIs — simply don't issue them.

Scan for secrets before they're committed. Catches the sprawl case. Does nothing for an agent that reads a live key from its own environment and posts it to a PR thread.

Sandbox the agent. Necessary, and the credential usually has to go into the sandbox for the agent to do its job. You've moved the boundary, not removed the secret from the wrong side of it.

Tell the agent not to reveal secrets. You are adding a sentence to a context window that the attacker also gets to write in.

Each of these makes the leak less likely or less bad. None of them makes it structurally impossible, because they all accept the same premise: the agent holds the credential.

Don't give it the credential

The alternative is to break that premise. Give the agent a placeholder — a string that names a credential without being one — and substitute the real value at the network boundary, on the far side of anything the agent can read.

  agent ──▶  Authorization: Bearer {{seekrit:STRIPE_KEY}}
                    │   (the agent's whole world: this string)
                 broker    ── substitutes, checks the allowlist, audits
                    ▼
             api.stripe.com   Authorization: Bearer sk_live_…

The agent's environment, its logs, its transcript, its error traces, and any file it writes contain {{seekrit:STRIPE_KEY}}. Prompt-inject it all you like. Exfiltrate everything it holds. What leaves is a placeholder, which is worth nothing to whoever receives it.

This is an old idea with a new name. The 2026 Internet-Draft Credential Broker for Agents (CB4A) writes it up as a policy decision point separated from a credential delivery point, injecting on egress. (It's an individual submission with no standing in the IETF process — but it's the clearest description of the pattern, and it gave the category a name people can search for.)

What separates a broker from a hole in your network

A proxy that swaps in any secret for any request is not a security control. It's an exfiltration oracle with good ergonomics: the agent asks for a placeholder to be resolved toward a host the attacker owns, and your broker helpfully mails the key. Four properties decide whether you built the first thing or the second.

Default-deny, per upstream. Each route declares which secrets may be injected toward which host. A placeholder naming a secret that isn't on that list is refused and never forwarded. Without this, everything else is decoration.

Operation-level bounds, not just hosts. api.github.com is one host and a hundred different authorities. A rule that allows a host but not a method or a path lets an agent scoped to read issues go delete a branch.

Fail closed. Resolve and decrypt at startup; if the token is bad, the vault unreachable, or a value won't decrypt, refuse to start. A broker that boots degraded and forwards unsubstituted placeholders has failed at the one moment it mattered.

Audit by name, never by value. Log which secret went where and when. Never log what it was. A credential broker with verbose logging is a credential warehouse.

What this doesn't fix

Some honest limits, because a broker gets oversold.

It doesn't stop an agent from misusing access it legitimately has. If the agent may call POST /v1/refunds and is talked into issuing a refund, the broker's job was to allow that. This is the confused deputy problem, and the allowlist narrows it rather than solving it.

It moves the target. The broker now holds every plaintext credential in one process. That's a real concentration of risk, and the mitigation is that it's a small, boring program you run yourself rather than a large one with a plugin system.

And it's weaker in-process. Any shim living inside your own application — a fetch wrapper, an HTTP transport — shares an address space with the code it's protecting, so anything that can call it can generally also read around it. Convenient, genuinely useful, not the same boundary as a separate process.

Try it

seekrit ships this as seekrit-proxy. One command, no install:

npx -y @seekrit/cli proxy run --preset openai

Point HTTPS_PROXY at it, or use it as a reverse proxy in front of a base URL. The agent sends placeholders; allowlisted upstreams get real credentials; everything else gets a 403.

The part specific to us: seekrit is zero-knowledge. Your secrets are encrypted in your browser or CLI before they leave the machine, so the service storing them holds ciphertext and nothing else. That matters here more than it looks — a broker protects your agent from the credential, but if the vault behind it can read your secrets you've relocated the risk rather than reduced it. The egress policy is signed client-side too, and verified against signers pinned in the proxy's own config, so we can't rewrite where your credentials are allowed to go either.

If you'd rather run the whole thing yourself, Infisical's agent-vault is MIT-licensed and solves the brokering half well. The pattern matters more than whose implementation you pick. What matters most is the part underneath both of them: stop handing reusable secrets to systems that can be talked into sharing them.