# A pull request told Amazon Q to wipe the machine. We recreated the injected PR — and the destructive calls never ran.

In 2025 a destructive instruction was slipped into the Amazon Q VS Code extension through a GitHub pull request, directing the agent to wipe the local filesystem and delete cloud resources using the developer's own permissions. We rebuilt the injected-PR mechanism in a sandbox. The governed twin denies rm -rf and destructive cloud calls no matter where the instruction came from.

<!--
EDITORIAL — published:false pending David's read. Console deny is a REAL capture
(recreated-amazonq identity, Bash.gcloud denied at background tier, live acp-ops workspace).
Captured honestly: the destructive step is a `gcloud compute instances delete` stand-in for
the incident's cloud-resource deletion, denied by acp-ops's STANDING policy (no demo rule) —
caption says so; the rm -rf half is referenced, not screenshotted. Ungoverned "victim" half
described in-text. Never author a working injection payload — convey intent, mechanism only.
-->



In 2025, an attacker slipped a destructive instruction into the [Amazon Q Developer extension for VS Code through a GitHub pull request](https://www.upguard.com/blog/mcp-security-incidents). The injected text directed the agent to wipe the local filesystem and delete the developer's cloud resources. Because the agent acted with the developer's own permissions, the instruction was, from the operating system's point of view, indistinguishable from a legitimate command the developer had typed.

Two things made this dangerous, and neither is the model. First, the instruction entered through **untrusted content the agent ingests as part of normal work** — a PR, the kind of thing coding agents read all day. Second, the agent held **destructive-grade permissions over the local machine and the cloud account**. The prompt was untrusted; the hands were fully privileged.

You cannot vet every PR, README, issue, and comment an agent reads for hidden instructions — that surface is unbounded. What you can do is make the *destructive actions* themselves require a check, so that it stops mattering who asked. We rebuilt the injected-PR mechanism in a sandbox to show the difference.

## The property that made it destructive

The incident is a clean statement of a single principle: **an agent should not be able to run irreversible commands just because some text it read told it to.** `rm -rf` on a home directory and "delete these cloud resources" are not operations whose safety should depend on the trustworthiness of a pull request. They're operations that should require a check every time, because the cost of being wrong is total and permanent.

The injected PR is the trigger. The destructive Bash and cloud calls are the damage. The trigger is unfilterable; the calls are governable. That asymmetry is the whole design argument.

## What we recreated

An isolated sandbox: a disposable working directory (never a real path), a mock cloud API with deletable fake resources, and an agent doing ordinary PR-review work.

1. A pull request carries a destructive instruction (defanged — we convey the intent to redirect the agent, not a working injection string).
2. The agent ingests the PR as part of its task.
3. The agent holds permissions to run destructive filesystem and cloud commands.

Ungoverned, the sandbox agent follows the PR: it issues the destructive command, and the disposable directory and mock resources are gone. The agent did nothing it wasn't permitted to do — that's the problem. Now the same run with a control plane in the path.

## The governed twin

Same agent, same poisoned PR, same permissions. This time the destructive call passes through policy — and the policy does not consult the *source* of the instruction, because the source is exactly the thing you can't trust. The real rule from our workspace: the destructive cloud CLI is denied at the `background` (autonomous) tier.

```json
{
  "mode": "enforce",
  "tools": {
    "Bash.gcloud": {
      "background": { "permission": "deny" }
    }
  }
}
```

In our sandbox the destructive step is a `gcloud` deletion of cloud resources — the half of the Amazon Q incident that erased the developer's cloud infrastructure. The governed twin runs at the background tier and its call is:

```bash
gcloud compute instances delete build-agent-vm --zone us-central1-a --quiet
```

The agent reads the poisoned PR and reaches the same instruction. The destructive command leaves the agent, hits the control plane, resolves as an irreversible cloud-resource deletion from an unattended agent, and is denied. The PR's authorship is irrelevant to the decision — which is the design: a control that only trusts "good" sources fails the moment an attacker learns to look like one.

<figure style="margin:28px 0 32px;">
  <img src="/assets/img/screenshots/recreated-amazonq-detail.png" alt="ACP console showing the denied cloud-resource-deletion call in full detail: verdict, background tier, identity, session, working directory, and the exact command" style="width:100%;height:auto;border:1px solid var(--line-2);border-radius:10px;box-shadow:0 20px 50px -24px rgba(0,0,0,0.9);margin:8px 0;" />
  <figcaption style="font-size:12.5px;color:var(--acp-text-faint);text-align:center;margin-top:10px;max-width:660px;margin-left:auto;margin-right:auto;">The real deny, with the full audit record: verdict, <strong>Background</strong> tier, identity, latency, session, working directory, and the exact command (<code>gcloud compute instances delete build-agent-vm</code>). The governed twin (identity <code>recreated-amazonq</code>) — driven by a poisoned PR — attempted the destructive cloud-resource deletion; the workspace's standing policy denied it, regardless of where the instruction came from. Captured from acp-ops; the command is a <code>gcloud</code> stand-in for the incident's cloud-resource deletion.</figcaption>
</figure>

(The incident's other half — an `rm -rf` filesystem wipe — is a destructive local command; the same source-agnostic principle applies, and denying destructive filesystem commands is covered in [Stop your agent from running rm -rf](/blog/stop-your-agent-from-running-rm-rf).)

## What ACP prevents here — and what it doesn't

- **ACP does not scan pull requests for hidden instructions.** That surface is unbounded and we don't pretend to filter it. ACP treats every prompt source as untrusted by default and governs the action instead.
- **ACP does not remove the developer's permissions.** The developer keeps their access; ACP adds a check at the point of the destructive call, so holding the permission and exercising it unsupervised stop being the same thing.
- **ACP stops the irreversible calls from executing unchecked.** `rm -rf`, `dd`, fork bombs, and destructive cloud mutations require approval (interactive) or are denied outright (background) — regardless of whether a human, a PR, or a README asked for them.

The Amazon Q agent wasn't hacked in the classic sense. It read some text and ran a command it was allowed to run. Every prompt-injection incident has that shape, which is why the durable defense isn't better prompt hygiene — it's a boundary at the action that no upstream text can move.

## Try it against your own setup

If your agent reads PRs, issues, or docs *and* can run shell or cloud commands, the injected-instruction path is open by default — the test is whether a destructive command survives the trip to your machine. I think of the check that stops it as the agentic control plane: it governs the action, not the sender, so a poisoned PR can't borrow your permissions to do something permanent. [Getting started](/getting-started) covers the install; the destructive-action policy above is the config.

*Source: [UpGuard — Six MCP Security Incidents](https://www.upguard.com/blog/mcp-security-incidents).*
