A signed release, eight hours, four thousand machines
On February 17, 2026, at 3:26 in the morning Pacific time, someone published cline@2.3.0 to npm. The CLI binary was byte for byte identical to the previous release. The only edit was one line in package.json: a postinstall script running npm install -g openclaw@latest. Roughly 4,000 developers installed it in the eight hours before the npm token was revoked and the version pulled.
Nobody phished a maintainer. Nobody guessed a password. The token that signed that release had been lifted out of a CI workflow by an AI agent that Cline itself was running to triage GitHub issues. The entry point was an issue title.
Five steps from a sentence of text to a signed package
Security researcher Adnan Khan disclosed the chain on February 9, 2026 and named it Clinejection. None of the pieces are novel on their own, which is exactly why it is worth walking through.
An attacker opens an issue on a public repository with a title crafted to read like a triage request. Cline's Claude-powered triage workflow picks it up, treats the title as an instruction, and runs an npm install from a commit the attacker controls. That package's preinstall script drops Cacheract, which poisons the CI cache. The nightly publish workflow later restores the poisoned cache, and the credentials it holds for npm, the VS Code Marketplace, and OpenVSX leave the runner without a single secret ever being read directly.
Cline patched the triage workflow inside about 30 minutes, in PR #9211, and started rotating credentials the same day. When it rotated, it deleted the wrong token. The exposed npm publish token stayed active for another eight days. That detail matters more than any of the clever parts. The patch was fast, the incident still happened, because rotation is a manual process with human hands in it and no alarm that fires when it goes wrong.
Why one sentence carries that much weight
A language model has no separate channel for data and instructions. Everything placed in the context window is text to act on. When the context window is filled from a GitHub event, and that event is authored by a stranger, the stranger is writing part of the system prompt whether anyone intended it or not.
GitHub metadata is user controlled by design. A free account can open an issue or a pull request against any public repository. Before agents entered pipelines, hijacking a widely used action took write access to that action's repository, and the March 2025 compromise of tj-actions/changed-files is what that looks like when it happens: roughly 23,000 repositories pulling a poisoned dependency before anyone noticed. AI agents remove the prerequisite. What is left is an account and a sentence that reads like a job.
Three configurations that turn a workflow into a launchpad
Bot identity treated as trust
Anthropic's claude-code-action shipped a checkWritePermissions function that let any actor whose name ended in [bot] trigger the agent. The assumption was that bot actors are apps the repository owner installed. GitHub Apps get implicit read access to public repositories and can open issues with a plain installation token, so an attacker registers an app, installs it on a repository they own, and opens an issue wherever the action runs. The permission check passes and the payload rides in the body.
From there, Claude Code runs cat and head without asking anyone, so reading /proc/self/environ sits inside its normal lane. That file exposes the workflow's environment variables, including the OIDC request token and URL, which can be exchanged for an installation token with write access to the action's own repository. The result is CVE-2025-66032, CVSS 3.1 score 8.7. Anthropic had a fix out within four days of RyotaK's report and shipped hardened builds in claude-code-action v1.0.94 and @anthropic-ai/claude-code v1.0.93. Repos still pinned to @v1 kept running the vulnerable code.
Version tags instead of commit SHAs
A tag is a movable pointer. Anyone who gains write access to an action's repository can repoint @v1 at a different commit and every consumer picks it up on the next run. Pinning to the full 40-character SHA closes that, and Dependabot still surfaces the update as a diff somebody can read before merging.
One credential scope for every job
If the job that reads untrusted text can also publish a package, sign a browser extension, or mint an OIDC token, then the only barrier between an issue title and a bad release is how well a model resists suggestion. Cline's publish credentials were scoped to the publisher account rather than per environment or per extension, so a nightly release token could authorize a production publish.
Three vendors, one mistake
Weeks after Khan's writeup, researcher Aonan Guan published findings under the name Comment and Control. The same pattern turned up in Anthropic's Claude Code Security Review action, Google's Gemini CLI Action, and GitHub's Copilot Agent: user-written GitHub metadata interpolated into a prompt with nothing in between.
In Claude's case, pull request titles went in unsanitized. A bash command placed in a PR title was executed, and its output, live API keys included, came back as a comment on a public repository. Gemini's action could be pushed off its safety guidelines by planted "Trusted Content Sections" in issue comments and printed API keys in the reply. Copilot Agent was the most evasive of the three. The payload sat in hidden HTML comments that reviewers never see but the model parses, the environment filter only cleaned child bash processes while the parent Node process stayed readable through ps auxeww, and output was base64 encoded to slip past GitHub's secret scanning in logs.
Across the three, the credentials collected included ANTHROPIC_API_KEY, GEMINI_API_KEY, a GITHUB_TOKEN with write scope, GITHUB_COPILOT_API_TOKEN, GITHUB_PERSONAL_ACCESS_TOKEN, and COPILOT_JOB_NONCE. Aikido Security, which named this class PromptPwnd, counted at least five Fortune 500 companies running workflows consistent with the pattern by mid-2026.
Anthropic's remediation is worth copying because it is layered rather than clever. A checkHumanActor step. Run summaries off by default so the workflow output stops being an exfiltration channel. A gh wrapper that validates arguments against known exfiltration URLs. Environment variables scrubbed from child processes. Four controls in four different places, which is the honest shape of this problem. There is no single chokepoint to guard.
An audit to run on your own repositories this week
Most of this is grep and reading YAML. None of it needs a pen test.
- Search every workflow for allowed_non_write_users: "*". It removes the human actor requirement. Treat a match as a finding, not a preference.
- Every uses: line ending in @v1, @v2, @main, or @latest is a mutable dependency. Replace with full commit SHAs, starting with anything on the release path.
- List workflows declaring id-token: write or contents: write, then check whether the same job also reads issues, pull requests, or comments. Both at once means the job needs splitting.
- Trigger types are a good filter. Workflows on issues, issue_comment, and pull_request_target are the ones where a stranger's text reaches a model.
- Ask what the runner can reach over the network. Egress filtering kills most exfiltration even when an injection lands.
- Secret scanning and push protection belong on every repository running an agent, and an alert in a workflow log is an incident, not a noisy rule.
The half fix that catches people is the environment scrub. Cleaning variables for bash children while the parent Node process still holds them looks like a fix in a code review and fails against anyone who has read the writeup.
Separate the reader from the doer
Version bumps close the CVEs that got reported. They do not touch the architecture that made an issue title dangerous in the first place.
The design that holds up puts the model somewhere it cannot act. An agent reads the issue, the diff, or the alert and returns a structured recommendation: a classification, a proposed comment, a list of files to change. A separate execution layer holding the credentials and the policy decides whether to do anything with it. That layer never parses free text from outside.
The comparison is old and boring on purpose. Nobody puts database credentials in a browser front end, because the front end runs on a stranger's machine. A prompt-injection payload is the same kind of visitor. It arrives from somewhere you do not control and gets read by something that does.
Two controls make the separation real. Human approval through GitHub environment protection rules on any workflow that can publish, tag, or deploy. And hard limits on spend and steps, with a kill switch that works at 3am, because an instruction saying "retry until it succeeds" will be retried until it succeeds.
What to ask a vendor before the agent touches your pipeline
Written answers, not a sales call.
- Where is the trust boundary documented, and what exactly does the agent hold?
- Has prompt injection been tested adversarially, or only listed in a threat model?
- Can the agent publish, tag, or deploy, or only propose?
- How are its credentials scoped, and can a token from one environment authorize another?
- What does it log, where does that log live, and can untrusted text end up in a public summary?
- Who can pull the kill switch, and how long does it take to take effect?
A vendor that has done the work answers these in specifics. A vendor that has not answers with adjectives.
The timing is the part to remember
Disclosure on February 9. Patch within the hour. Exploitation on the 17th. What failed in between was not code. A rotation deleted the wrong token, and nothing checked the result.
AI agents in CI are not a product category you buy your way out of. They are a privilege decision, made once per workflow, usually by whoever wrote the YAML last. Decide what the thing that reads public text is allowed to do after it finishes reading, and most of this class of incident turns back into a suggestion nobody merges.