Prompt Injection Through Tool Output Is Two Events (Your Screens Read One)
Tool output is untrusted because your own systems produce it. That is the part of...
Sep 8, 2026
The blast radius of a prompt injection against your coding assistant was set weeks ago, by whoever built the dev environment image. Same assistant, same model, same injected sentence: on a laptop it collects every repository, SSH key and cloud login the developer holds; on a provisioned dev box it collects an organization token plus whatever the image left behind; on a CI runner it collects a deployment credential and a network path to production. Three environments, three incidents, one payload. The assistant is the only variable that stayed constant. Same payload, three different distances.
What follows is what the assistant already holds in each of the three environments, the six channels an outsider can write to in order to reach it, the matrix those two produce, and the three things you set that change any cell in it.
Reach is a product of three quantities: the tools the assistant can call, the credentials it holds, and the destinations its network allows. The vendor sets the first. Your platform team sets the other two, usually inside an environment image, usually long before anyone asks a security question about it.
| Environment | What the assistant inherits | Who is at the approval prompt | What a successful coercion collects |
|---|---|---|---|
| Developer laptop | The engineer’s whole working set: cloned repositories, SSH keys, shell history, live cloud CLI sessions, credential helpers | The developer, when they are watching | Anything that engineer can reach, which on a senior engineer’s machine is most of the estate |
| Cloud dev box, provisioned per task | A git token scoped to the organization, registry credentials, and whatever the image baked in: a kubeconfig, a cloud role, an observability key | Nobody. Auto-approve is set at the environment level | The organization token and the image residue |
| CI runner | A deployment credential, signing material, a network route into staging or production | Nobody, by design | Publish and deploy paths |
Read the second column as a provisioning decision, because that is what it is. Nothing in it was chosen by a security team, and nothing in it is visible from the assistant’s settings page.
The three rows are not a maturity ladder. Teams move agents off laptops onto dev boxes for throughput, and the move trades a very wide blast radius that somebody occasionally watches for a narrower one that nobody watches at all. Which of those is worse depends entirely on the fourth column.
The obvious objection is that the laptop case belongs to endpoint policy, and it does. Policy is what decides what sits in the image and what the token can open, which is the same audit either way.
What makes any of this a security incident rather than a provisioning quirk is the last step. An instruction arrives from a source trusted for content, a model treats it as an instruction, and the application acts on it with borrowed authority. Remove the credential and the same sentence produces a strange paragraph.
A coding assistant assembles its context from six sources during a single task, and every one of them accepts writes from someone outside your organization.
| Channel | Who can write there | When the assistant reads it | Asked or unasked |
|---|---|---|---|
| Repository files: README, CONTRIBUTING, rule files, and the setup scripts they point at | Anyone who publishes a repository | On clone, before the task | Unasked |
| Dependency metadata and install hooks: manifests, post-install scripts, lockfile URLs | Any package maintainer, any compromised maintainer account | During install and build | Unasked |
| Issue and pull request bodies | Any account on a public tracker | When the task is triage or review | Asked |
| CI and test output | Anyone whose code runs in the pipeline | When the assistant reads a failure to debug it | Asked |
| Documentation fetched mid-task | Anyone running a site the assistant browses | On lookup | Asked |
| Output from a sibling agent | Whoever coerced the first agent | On handoff in a fan-out | Unasked |
The payload space behind those six is unbounded. The list itself is six rows long, and six rows is a thing you can audit.
Row one is settled ground. The README case walks a repository file from clone to leaked token, and the same indirect injection path applies to every unasked row beneath it. Researchers who first demonstrated injection through retrieved content called the attacker’s position remote: no access to the interface, only to something the model would later read. A public repository is the purest version of that position, and rows two and six are close behind.
Row two deserves separating from row one, because the two get collapsed. A setup script a README points at is a repository file, and someone cloning that repository chose to. A post-install hook in a package manifest runs on machines that never cloned anything the attacker published.
Row six is the newest and the least instrumented. When one agent hands work to another, the second agent inherits the first agent’s context along with the first agent’s compromise, and the combined reach is the union of both credential sets. The same shape shows up in the tool descriptions an assistant loads at startup.
The empirical evidence covers one row. AIShellJack, the largest published evaluation of injection against coding editors, built 314 payloads across 70 MITRE ATT&CK techniques and ran them against Cursor and GitHub Copilot. Attack success ranged from 41% to 84% depending on editor and model. In the Cursor Auto configuration, credential-access techniques succeeded 68% of the time across five codebases.
Two of its findings matter more than the headline number.
The first is that what the developer typed barely moved the result. Across five rephrasings of the task, including one that never referenced the poisoned file, success held between 17 and 19 out of 20. The assistant located the file on its own, because locating relevant project resources is its job.
The second is that framing matters and disguise does not. Payloads stripped of their contextual wrapper dropped to near zero, while payloads that kept a plain operational justification held. The wrapper that works is the one an honest project already writes: this is a required first step, run it before making changes.
The paper’s authors name their own limit. They evaluated rule files, and note that project templates, third-party libraries and MCP servers remain untested. Five of the six rows above have no comparable number attached to them, which is a gap in the literature rather than an absence of exposure.
Cross the six channels with the three environments and each cell holds one question: if this channel carries an instruction here, what does the assistant reach.
| Channel | Laptop | Cloud dev box | CI runner |
|---|---|---|---|
| Repository files | Local keys, every cloned repo | Org token, image residue | Build scripts run; rule files rarely load |
| Dependency metadata and hooks | Local keys, plus persistence | Registry credentials | Deploy credential at build time |
| Issue and PR bodies | Whatever triage touches | Org token | Not read |
| CI and test output | Secrets printed into local logs | Org token | Pipeline variables |
| Fetched documentation | Unrestricted egress | Allowlisted, or not | Usually blocked |
| Sibling agent output | Union of both sessions | Combined token scope | Chained deployment |
Those cells are a worked example. Yours will differ, and the difference is the point.
Three passes fill your own version. The first lists the environments you actually run, which is usually four rather than three, because one team never migrated off laptops and nobody closed the path. The second dumps, per environment, what the image leaves on disk and what each token can open; the declared list and the observed list diverge, often by a wide margin, and the delta is the row that matters. The third asks two questions per channel: can someone outside the company write here, and does the assistant read it without being asked. Two yeses is a live cell.
A live cell is a hypothesis, and hypotheses get tested. Ten tests against a scratch repository will tell you which rows your assistant completes today. What a completed row produces afterwards is the same eight stages any injected agent runs through, compressed into the few minutes a coding task takes.
Three of the four levers are provisioning decisions. The fourth is a detection decision, and it exists because the first three cannot close every cell.
Token scope is the repository list attached to the credential. A token scoped to the repositories the task names removes most of column two, and the work is enumerating tasks rather than tightening a setting.
Egress is the allowlist’s entries: the registries, the model endpoint, the artifact store. Anything reachable that is not on that list is a destination a coerced run can use, and package installs already opened the general path.
Image residue is the manifest of what the environment image leaves behind. A kubeconfig from a debugging session, a cloud role from a previous template, an observability key nobody removed. Residue is the cheapest cell to close and the least likely to be audited, because it was never deliberately granted.
Those three shrink the reach. They do not detect the run, and text-layer controls do not either, because the sentence that works reads like documentation. Published design patterns constrain which tool an agent calls while leaving the arguments open, and GitHub’s own guidance describes the approval prompt as one layer among several, listing file reads among the operations that never trigger one. Prompt injection has held the top position on OWASP’s LLM risk list since the list existed, and no vendor has shipped a model that separates instructions from data.
What remains is the assistant’s own behavior, and a coding assistant’s behavior is unusually narrow. Task after task it runs the same process tree, writes the same paths, contacts the same handful of destinations. ARMO builds a per-agent behavioral profile from what each workload executes, Application Profile DNA (APD™), and measures every new action against that agent’s own history rather than a fleet-wide rule. The case the profile exists for is coercion, where every step is permitted and the sequence is the attack.
Arming that profile follows progressive enforcement: record first, review what a per-agent policy would have blocked against real tasks, arm once the boundary holds. Credential isolation adds an outcome on top, where the assistant works with placeholders and the real credentials are substituted outside its reach, so an instruction that needs the token completes with a token that opens nothing.
ARMO’s runtime security for AI workloads covers the column the three levers cannot: what the assistant did, measured against what it does.
The inventory is eighteen cells, and it belongs to you.
Vendor comparisons move on a release cycle and expire with it. The inventory changes the day someone edits an image or widens a token, which makes it the artifact worth keeping current. It also answers the question a board asks, which is what an incident would have cost, rather than which product had the better quarter.
Bring one coding assistant and the environment it runs in to a working session, and the cells get filled from its actual runtime record instead of from judgment.
How far does prompt injection reach in a coding assistant? As far as the environment lets it, regardless of which assistant is running. None can structurally separate an instruction from the data it reads, so the vendor and model only narrow the odds, not the ceiling. Published evaluations show success rates varying by editor and model, from roughly 41% to 84% in the largest study to date, so the differences are real but none of them approach zero. Treat the variation as a tiebreaker and the environment as the actual control.
Does turning on approval prompts fix this? It works when a person is reading the prompt and declining. Teams move assistants to shared infrastructure so several can run unattended, auto-approve gets set at the environment level, and the prompt stops being a control at that moment. Any control that reinserts a person per step returns the assistant to laptop speed, which is the thing the migration was for.
What should a coding assistant in a shared environment be allowed to reach? A git token scoped to the repositories the task names, an egress allowlist holding the registries and the model endpoint, and writes confined to the workspace. Everything the environment image leaves behind is additional reach that nobody granted deliberately: kubeconfigs, cloud roles, registry credentials, observability keys. Where credential isolation is available, hand the assistant placeholders and substitute outside its process.
Can I scan the inputs before the assistant reads them? Scan, and keep scanning, because hidden comments and override phrases cost almost nothing to strip. It will not catch the payload that matters, which is a plain operational instruction pointing at a setup target, and the evaluations show that stripping the wrapper is what kills the attack rather than stripping the disguise. Scanning also only reaches the channels you can inspect, which excludes fetched documentation and sibling agent output.
How do I tell whether an injection already succeeded? Look for reads of credential paths the assistant has never opened, a first-seen outbound destination that is neither a registry nor the model endpoint, a process lineage this assistant has never produced, and a push or publish the task never named. Any one is a deviation against a narrow baseline. Two in the same run, in that order, is the sequence rather than a coincidence.
Tool output is untrusted because your own systems produce it. That is the part of...
Every action in your agent-incident runbook operates on the agent. The payload of a RAG...
Your company runs two kinds of AI copilot, and only one of them can be...