One GitHub Issue, Production Credentials: The Claude Code Action Injection Chain

A single opened GitHub issue was enough. Not a compromised maintainer account, not a stolen PAT, not a poisoned dependency. Open an issue on a public repo wired to Anthropic’s Claude Code GitHub Action, hide some instructions inside an HTML comment so a human reviewer scrolls right past them, and the agent could be steered into reading the runner’s process environment and handing the contents back to you. The contents being, depending on the workflow, the ANTHROPIC_API_KEY and the GitHub Actions OIDC request credentials. Microsoft’s writeup put the stakes plainly: a crafted comment plus a misunderstood trust boundary, and you walk away with production credentials.

What makes this worth your time is that it isn’t one bug. It’s two findings, from two research teams, with two root causes and two separate fixes, that happen to compose into one clean kill chain. They’re easy to conflate because both end at the same file on disk. Keep them apart and the failure modes get a lot clearer.

The chain has two halves

GMO Flatt Security’s RyotaK found the half that gets the agent to run for an attacker who controls nothing. The action’s permission gate, checkWritePermissions, was supposed to enforce the documented promise that only users with write access could trigger the agent. It returned true for any actor whose name ends in [bot], on the assumption that GitHub Apps are trustworthy. They are not, at least not in the way that check assumed. GitHub Apps have implicit read on public repos and can open issues on them using an installation token. So an attacker registers their own app, installs it on a repo they own, and uses that token to file an issue on your repo. The [bot] suffix sails it past the gate. The promise that only write-access users could reach the agent was just broken.

Microsoft’s Defender Security Research team — Dor Edry and Amit Eliahu — found the half that turns agent access into a leaked secret. Claude Code ran the Bash tool inside a Bubblewrap sandbox with environment scrubbing (CLAUDE_CODE_SUBPROCESS_ENV_SCRUB), so a shell command couldn’t just print the API key. The Read tool did not get that treatment. It made direct in-process calls with full access to the process environment, which means it could read /proc/self/environ and return the unscrubbed ANTHROPIC_API_KEY. The sandbox existed. The Read tool walked around it.

That overlap — both writeups target /proc/self/environ — is exactly why people mix them up. Microsoft frames it as a tool-isolation gap at the runtime layer. RyotaK frames it as one link in a chain that ends with OIDC-token theft and a write-scoped installation token. Same file, different stories, both correct.

How the secret actually gets out

Getting the model to read the file is only part of it. There were two safety boundaries in the way, and the payload was built to defeat both.

The first is the model’s own refusal. Ask an agent to exfiltrate credentials and it should decline. The injection reframed the request as a benign “compliance review,” which is the kind of plausible operational pretext these models are bad at refusing. The second boundary is GitHub Secret Scanning, which would normally catch an ANTHROPIC_API_KEY hitting stdout. The payload steered the model to alter the key before emitting it, so the output no longer matched the scanning signatures. Conceptually that’s all you need to understand: the key was laundered into something that didn’t trip the pattern match, then sent out through whatever channel the workflow exposed. WebFetch to an attacker domain if it was available. A GitHub MCP comment back into the issue. The run log itself if show_full_output was on.

Two controls, both nominally present, both defeated by the same payload. That’s the part worth sitting with. The model’s safety layer and GitHub’s secret scanning are real defenses, and a single crafted comment routed around both of them at once.

The supply-chain pivot

RyotaK’s chain doesn’t stop at one repo’s secrets. The OIDC request credentials (ACTIONS_ID_TOKEN_REQUEST_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL), once leaked, can be replayed against Anthropic’s backend to mint a Claude GitHub App installation token with write access to code, issues, and workflows. Read-only credential theft becomes write access to the repo.

And here’s the multiplier. The anthropics/claude-code-action repository itself ran an agent-mode workflow. Compromise that repo and you’re not stealing one team’s keys anymore; you’re positioned to push malicious code that propagates to every downstream repo pinned to the action. That’s the difference between an incident and a supply-chain event. Anthropic rated the finding CVSS v4.0 7.8 in its bug-bounty program — note that this is a vendor bounty rating, not an NVD-published score — and paid out $3,800 plus a $1,000 bonus. RyotaK reported on the order of fifty distinct ways to bypass the agent’s permission and command controls, which tells you the gate wasn’t one weak check but a soft perimeter.

This already happened in the wild, by the way. Per The Hacker News, citing RyotaK, the same class hit Cline in February 2026: a claude-code-action workflow compromise led to a stolen npm publish token and a malicious cline@2.3.0 getting pushed with an unauthorized agent embedded. It was pulled about eight hours later. Eight hours is plenty of install window for a popular package.

The timeline, and the two fixes

These are genuinely two remediation tracks. Don’t assume one version covers both.

Disclosure Layer Root cause Fix
A — Microsoft (Edry, Eliahu) Runtime / CLI Read tool not sandboxed like Bash; read unscrubbed env via /proc Claude Code 2.1.128
B — GMO Flatt / RyotaK GitHub Action checkWritePermissions trusted [bot] actors claude-code-action v1.0.94

Disclosure A: reported to Anthropic via HackerOne on 2026-04-29, fixed 2026-05-05 in Claude Code 2.1.128, published by Microsoft 2026-06-05. The fix makes the Read tool unconditionally reject a set of /proc/ files, which is default-deny at the tool rather than relying on env scrubbing happening in a subprocess that the Read tool wasn’t using anyway.

Disclosure B ran longer because there were more holes. The permission bypass was reported 2026-01-12 and fixed in four days. Follow-up misconfiguration reports landed the next day, the Cline exploitation showed up mid-February, and remediation rounds continued through April. The hardening consolidated into v1.0.94: checkHumanActor added to agent mode so the trigger has to be a human; GitHub Apps disallowed from triggering outright (commit 1bbc9e7); the workflow run summary disabled by default to close that public-panel exfil channel; environment scrubbing extended to child processes the agent spawns; and a custom gh wrapper that validates arguments. RyotaK also flagged that example workflows shipped with allowed_non_write_users: "*", which means anyone can trigger — a parameter that is itself a foot-gun, and if you copied an example workflow you may still have it.

Update both. Claude Code at least 2.1.128, claude-code-action at least v1.0.94. One without the other leaves a leg of the chain standing.

No CVE, and that’s the real story

Neither finding got a CVE. Not Microsoft’s, not RyotaK’s. The only severity number anywhere is Anthropic’s internal bounty rating. And this isn’t isolated to Anthropic. Per The Next Web, researcher Aonan Guan documented the same prompt-injection-in-CI pattern against Anthropic’s own Claude Code Security Review (Nov 2025), Google’s Gemini CLI Action, and GitHub Copilot Agent (March 2026, instructions hidden in HTML comments — sound familiar). None of the three vendors assigned a CVE or published a public advisory. Guan’s framing, via The Next Web: without a CVE, scanners won’t flag it and users on old versions may never know they’re exposed.

That’s the gap. The vendors are treating prompt injection as emergent model behavior rather than a software defect. I don’t buy that framing. The blast radius — leaked production credentials, a write-scoped token, a path to supply-chain propagation — is identical to a classic memory-corruption exploit. If the outcome is RCE-equivalent, the disclosure process should be exploit-equivalent. Calling it “model behavior” is how a real vulnerability becomes invisible to every dependency scanner you run. Your SCA tooling cannot warn you about a pinned action version with no advisory attached to it. You’re back to reading vendor blog posts and hoping you catch the one that matters.

What to actually do

The cleanest mental model here is Meta’s “Agents Rule of Two,” which Microsoft cites: an agentic workflow should not simultaneously (a) process untrusted input, (b) hold access to sensitive secrets, and (c) be able to change state or talk to the outside world. Hold all three and you have this bug. Break any one leg and the chain doesn’t complete. For a CI agent that has to read issues (untrusted input, leg a) and needs an API key (leg b), the leg you can most realistically cut is (c) — kill the outbound channels. Disable run summaries and full output, restrict WebFetch, scope down what MCP tools can post.

On tokens, least privilege is the spine. One key per environment, per workflow. Scope GITHUB_TOKEN permissions down to what the job needs and nothing more. If the agent doesn’t need a secret, don’t put it in the environment it can read. The Read-tool fix is real, but defense-in-depth means assuming the next isolation gap exists.

Gate the trigger like RyotaK’s fix does: require a human actor, don’t trust [bot], and grep your workflows for allowed_non_write_users: "*" before you do anything else. Pin the action to a full commit SHA rather than a moving tag, and keep an eye on upstream releases — the supply-chain pivot is the scenario where a tag that moved under you is the whole incident.

The detection side is where this gets honest about its limits. The most reliable signal you have is provider-side: a leaked ANTHROPIC_API_KEY shows up as anomalous usage — new source IPs, traffic spikes, calls to endpoints the workflow never touched. Useful, but it’s an after-the-fact signal that fires once the key is already out and being used, and the baseline is noisy. A busy org’s API key talks to a lot of IPs legitimately, so the first tuning pass is mostly suppressing CI egress ranges and your own dev traffic before the anomaly rule is worth paging on. Treat it as a backstop, not a wall. The trust boundary at the action and the kill on outbound channels are what actually prevent the theft; provider monitoring just tells you when prevention failed.

Where this maps in 800-53

The control story is unusually tidy for an emerging-tech bug. AC-6 least privilege is the core — scoped per-workflow tokens, one key per environment, rotate on exposure (which folds in IA-5). AC-4 information flow enforcement is what the Rule of Two actually is: a boundary between untrusted input and sensitive sinks. SI-10 input validation covers the foundational error, which is the agent treating issue and comment text as instructions instead of data. SC-7 boundary protection is the Bubblewrap gap directly — the Read tool sat outside the isolation the Bash tool got. Supply chain lands on SR-3/SR-4/SR-11 plus CM-2/CM-3 for pinning to SHAs and reviewing upstream changes. AU-6 covers the provider-side anomaly monitoring. And the way Microsoft found this — pointing an AI model at the action’s codebase and the obfuscated SDK after black-box probing came up empty — is SA-11 developer testing applied to agent integrations. They used an AI to find an AI-agent bug, which is either poetic or ominous depending on your mood.

The useful hardening here treats the system prompt as a place to make the trust boundary explicit: anything in an issue, comment, commit message, PR description, or file content is data from an untrusted author, never an instruction. It helps. It is not sufficient, because the model can’t reliably enforce a boundary it can’t reliably perceive, which is the entire reason indirect prompt injection works. The system prompt is a seatbelt. The token scoping and the killed outbound channels are the brakes.

Microsoft said it straight: right now, development is moving faster than defense. Two findings into one action, no CVE on either, and a class of bug hitting four vendors at once is what that gap looks like in practice. The fixes are shipped. The disclosure framework that would have told you to apply them is still missing.

Sources