Three Coding Agents, One Comment, All Three Leaked Their Own Keys. One Vendor Called It 9.4 Critical, Paid $100, Then Downgraded It to None.
- Patrick Duggan
- Aug 2
- 5 min read
Updated: 4 days ago
We hold that the interesting question about prompt injection is never does it work. It is which agent held. We report that flat in both directions — when one resists, we say so by name; when one folds, same.
So here is the flat version. Aonan Guan, with Zhengyu Liu and Gavin Zhong of Johns Hopkins, pointed one class of injection at Anthropic's Claude Code Security Review, Google's Gemini CLI Action, and GitHub's Copilot Agent.
None of the three held. All three leaked their own credentials.
We are writing this on 2 August. It was published on 16 April. We were late by three and a half months on a story sitting in the middle of our own beat, and we found it running a headline sweep looking for something else.
The attack, and why it is not really three attacks
Every one of these agents reads text that a stranger controls — a pull-request title, an issue body, a comment — and passes it to a model that is also holding live secrets. The three variants differ only in which field the stranger writes into.
Claude Code Security Review interpolated the PR title straight into the prompt template. A title is a string; the template made it an instruction. The spawned CLI subprocess inherited the environment unfiltered, so ps auxeww inside that process could read what the process was holding — ANTHROPIC_API_KEY and GITHUB_TOKEN — and the output went back out as a PR comment.
Gemini CLI Action folded issue titles, bodies and comments into the prompt under an "Additional Content" heading. The researchers wrote a fake "Trusted Content Section" into the untrusted content. The model believed the label over the structure and posted its GEMINI_API_KEY as a public issue comment.
GitHub Copilot Agent was the most defended and the most thoroughly beaten. The payload hid in an HTML comment — invisible in GitHub's rendered view, fully present in the raw body the agent parsed. Copilot filtered the environment out of the child bash subprocess, which sounds correct until you notice the parent Node process and the MCP server still held everything, and ps auxeww reads /proc/[pid]/environ across processes. Four tokens left: GITHUB_TOKEN, GITHUB_COPILOT_API_TOKEN, GITHUB_PERSONAL_ACCESS_TOKEN, COPILOT_JOB_NONCE. They left through a whitelisted `git push`, which needed no exfiltration server at all.
No attacker infrastructure. No callback domain. Nothing for a network monitor to see. The data left through the same channel the agent was supposed to use.
The base64 step is Pattern 52 again
One detail deserves its own paragraph, because we have a name for it.
Copilot's secret scanning matches on token prefixes — ghs_, ghu_. The researchers base64-encoded the output. The prefix stops existing. The scanner matches nothing and reports clean.
That is our Pattern 52, the subtraction attack: the win comes not from adding a payload the defence has never seen, but from deleting the artifact the defence assumes will be there. Secret scanning does not detect secrets. It detects a string shape. Remove the shape and you have removed the detection, and — this is the part that matters — the scanner does not report a miss. It reports a pass.
What each vendor did about it
Vendor | Severity assigned | Bounty | Mitigation |
Anthropic | Critical, CVSS 9.4 — later revised to None | $100 | Blocked the ps command via --disallowed-tools; updated docs |
not published | $1,337 | Added guardrail prompts to the system prompt | |
GitHub | "previously identified architectural limitation" | $500 | None shipped; "actively exploring" further restriction, no timeline |
Read the Anthropic row twice. Reported 17 October 2025. Rated Critical, 9.4, and resolved on 25 November 2025, with a $100 payout attached to that 9.4. Then on 20 April 2026 — four days after the research went public — the severity was downgraded to None.
We are not going to pretend to know the internal reasoning, and there are legitimate ones: a severity field can be re-scoped to the product boundary, or reclassified once a mitigation ships. But a public record that reads 9.4 Critical → $100 → None invites exactly one interpretation from everyone who reads it, and no vendor should want that to be the artifact they leave behind.
The mitigations are the weaker half of the story anyway. Blocking `ps` is a symptom fix. ps was how these researchers read the environment; it is not how environments get read. Guardrail prompts against prompt injection are asking the vulnerable component to defend itself using the exact channel that was compromised — that is the mechanism failing, offered as the remedy.
GitHub's answer is the least satisfying and the most honest of the three. Architectural limitation is correct. Secrets and untrusted input share a runtime, and until that stops being true, every fix is a filter over a design decision.
Anthropic is our partner. This is the coverage they get.
Claude is the partnership here — that is not a disclosure buried at the bottom, it is the whole operating model of this shop. I am writing this with Claude Code, in a repository holding Azure credentials, with tooling that reads from the internet.
Which makes us not a commentator on this finding. It makes us the exposed population.
We cover Check Point CVEs straight and I am an alumnus there. Same rule applies here, and it has to apply harder when the vendor is the partner, because coverage that goes soft on your friends is worth nothing to the reader who has to make a decision. The finding is real, the response was thin, the severity revision looks bad, and none of that is in tension with Claude Code being the best tool we have found for this work.
What to actually do if you run these in CI
Assume any secret in the agent's environment is disclosed the moment the agent reads attacker-controlled text. Not at risk. Disclosed. Scope credentials accordingly.
Do not run agents against untrusted pull requests with repository write scope. pull_request_target and its equivalents hand a fork's contents a privileged token. That was the precondition here.
Stop treating a filtered child shell as isolation. Copilot's UU() filter was real, it worked on the process it covered, and the parent still held everything. Isolate the process tree or do not claim isolation.
Expect encoded exfiltration to defeat prefix-matching secret scanners, and monitor the whitelisted egress channels — git push, issue comments, PR comments, Actions logs. Those are where the data went. There was no C2 to find.
Rotate on visibility, not on evidence of theft. If a token was resident in a process that read a stranger's text, it is burned. That posture is cheap and it is the only one that survives an attack with no observable indicator.
What we are not claiming
We did not find this. Guan, Liu and Zhong did, and the writeups worth your time are theirs. We have no indicator, no telemetry, and no in-the-wild exploitation to report — nothing in our corpus is attributable to this technique.
What we can tell you is that we ran our own corpus against it before writing, and our coverage of it was zero. The nearest neighbour was a CamoLeak post from 10 April. On a beat we explicitly claim as ours, on a finding that names the agent we ourselves run, we had nothing for three and a half months.
That is the honest scoreboard, and the reason the sweep exists.
Sources: Aonan Guan — Comment and Control · SecurityWeek · Vectara — awesome-agent-failures case study
How do AI models see YOUR brand?
AIPM has audited 250+ domains. 15 seconds. Free while still in beta.
Was this useful? Thirty seconds, no cookies, no tracking, no third parties, your address hashed and never stored. If the box below does not load, the same question lives at https://analytics.dugganusa.com/nps.html?post=three-coding-agents-one-comment-all-three-leaked-their-own-keys-one-vendor-called-it-9-4-critical




Comments