Key Takeaway
Agentjacking is an indirect prompt injection: an attacker uses a public Sentry DSN to post a fake error event, and an AI coding agent reading it through the Sentry MCP server runs the command in its Resolution text. Tenet reports an 85% success rate. Defend with approval gates, sandboxing, DSN rotation and by treating telemetry as untrusted input.
The detail that stopped me was how little the attacker needs. No phishing, no stolen login, no foothold in your network: just a Sentry DSN that your own frontend already ships to every visitor. I read the three public accounts of this attack closely, along with the vendor docs and the agent docs that decide whether it works, and what follows is a defender's reading of them.
I did not reproduce the attack and this post contains no payload; the defences do not need one. Every figure below is attributed to the document I read it in, and where the sources disagree or go quiet I say so. The goal is a hardening checklist you can apply to any MCP server that returns telemetry, not just Sentry's.
A write-only key and a read-only tool, chained
Tenet Security's account is that an attacker needs only a public Sentry DSN, the credential Sentry documents as safe to embed in frontend JavaScript, to submit a synthetic error event. The message and the context key names carry markdown styled to look like Sentry's own diagnostic guidance, with a Resolution section that holds a command. When a developer later asks an agent to review Sentry issues, the Sentry MCP server returns that event next to the genuine ones. The CSA research note says the tested agents treated it as authoritative and ran the command with the developer's operating system privileges.
Neither half is a bug on its own. The DSN is meant to be public, and the MCP server returned what it was built to return. The CSA note frames the problem as architectural, resting on three design choices: permissive ingest, MCP forwarding without content integrity checks, and agents that trust tool output as much as operator instructions. It lists the credentials at risk as AWS keys, GitHub tokens, Kubernetes credentials, registry logins from .npmrc and .docker/config.json, and CI/CD environment variables.
What the numbers say, and what they leave out
I read Tenet's post, the CSA note and the Infosecurity Magazine report. The headline figures repeat across all three, but none publishes the underlying data. This table is the gap between what is claimed and what you can check.
| Claim | What the sources state | What I could not find |
|---|---|---|
| 85% success rate | Tenet reports 100+ agent instances in controlled validation waves and an 85% exploitation rate. CSA attributes it to Claude Code, Cursor and Codex CLI. | Trial count, denominator, per-agent breakdown and model names. The only version given is Claude Code 2-1-161. |
| At least 2,388 organisations exposed | Found by passive reconnaissance: Censys indexing, code search and CDN loader extraction. Exposed means a valid injectable DSN was found. 71 rank in the Tranco top 1M. | How many were actually attacked. Tenet says 2,221 were not in the validation set. |
| Sentry acknowledged on 3 June 2026 | Tenet and CSA both say disclosure and Sentry's reply happened that day, and both quote 'technically not defensible'. | A statement from Sentry itself. Both accounts relay the reply second-hand. |
| Publication dates | The CSA note is dated 2026-06-14 and cites Tenet as 9 June. Infosecurity ran on 11 June. The Tenet page I fetched is headed 17 June under a different headline. | Which date is canonical. I cite by URL and avoid leaning on the order of events. |
| Prompt-level defences fail | Tenet says they failed in its tests. Infosecurity reports agents ran the payload even when told to ignore untrusted data. | The prompts that were tried. |
This is vendor research and self-reported, so I would not plan around the exact 85%. I would plan around the direction, which matches how OWASP describes tool poisoning: a response the model reads as trusted when nothing checked it. Treat the rate as above zero and design for that.
Why a leaked DSN is not a leaked secret
Sentry's DSN explainer is blunt: DSNs are safe to keep public because they only allow submission of new events and related event data, with no read access. The same page admits the risk that matters here, that any user can send events to your organization with any information they want, calls it rare, and points to IP blocking plus the option to rotate and revoke keys under Client Keys (DSN) in project settings.
That advice was written for a world where a human reads events in a dashboard and junk is noise. Once an agent with a shell reads them, any information they want includes instructions. The Sentry MCP page I read says connections use OAuth and that you can scope the URL to one organization or project, recommending project scope; it has no security section and does not mention prompt injection. Scoping shrinks how much untrusted telemetry the agent pulls in. It does not make any of it trusted.
Sentry's response, and why a string filter is not a fix
Per Tenet and CSA, Sentry was told on 3 June 2026, acknowledged it the same day, called comprehensive platform-level fixes 'technically not defensible', and shipped a global content filter for the one payload string seen in the research. Remember the caveat from the table: I read that reply second-hand.
CSA argues a string match does not close the pathway, since altered wording or an indirect route can slip past it. OWASP agrees on the underlying difficulty: fully detecting injected instructions in free-text responses is an open problem. Sentry's position has a logic to it, because a platform cannot separate a hostile Resolution paragraph from a legitimate one without breaking error reporting. The control therefore belongs where the action happens, in the agent runtime, which is also Tenet's main recommendation.
Do not wait for a vendor fix. A content filter protects against a payload someone already published. Assume the next one is worded differently and put your control on the command, not the text.
Which controls hold, and which only feel like they do
Each control below stops something real and misses something specific. The expensive mistake is stacking several that fail the same way, for example trusting an approval prompt while a broad allow rule has already removed it.
| Control | What it stops | What it misses |
|---|---|---|
| A prompt saying ignore untrusted data | Nothing you can rely on. | Tenet saw it fail; Infosecurity reports agents ran the payload anyway. |
| Human approval for shell, installs and outbound requests | The execution step, if a person reads the command. | Claude Code's 'don't ask again' is permanent per repository and command, so one careless yes becomes a standing allow. |
| Deny rules for curl and wget | Exfiltration through those two programs. | The docs say a deny rule does not match the same program by path or inside sh -c, and argument patterns are fragile. |
| Sandbox with network allowlist and credential deny | Shell children reaching unlisted hosts or reading listed credentials, even if an injection bypasses the model's judgement. | It wraps shell commands only. File tools, MCP servers and hooks run outside it. |
| Rotating exposed DSNs | Posting with the old, already-harvested key. | The new key ships in the same frontend and is public again the moment you deploy. |
| Disabling the Sentry MCP server | The channel entirely. | You lose agent triage, and the next telemetry MCP has the same shape. |
The only row that holds regardless of what the model decides is the sandbox, and the Claude Code docs say as much: sandbox restrictions still apply even if a prompt injection bypasses Claude's decision-making. The permission rows are your first line, the sandbox is the one that does not depend on a person paying attention at 6 pm.
A Claude Code settings file that gates the step after the read
I would not block the read, because triage is the reason you connected Sentry. I would gate what an agent can do next. The permissions docs say rules are evaluated deny, then ask, then allow, the first match wins, and an ask rule still prompts when a more specific allow also matches. So an ask on npx survives a teammate's allow rule added in a different settings file.
// ~/.claude/settings.json or .claude/settings.json. Strip the comments: JSON has none.
{
"permissions": {
// ask is checked after deny and before allow, and it still prompts when a
// narrower allow matches. A teammate's allow rule cannot quietly undo it.
"ask": [
"Bash(npx *)",
"Bash(npm install *)",
"Bash(npm i *)",
"Bash(pip install *)"
],
// Deny is a belt, not the boundary: it does not match the same program by
// path or inside sh -c, so the sandbox allowlist below is what must hold.
"deny": [
"Bash(curl *)",
"Bash(wget *)",
"Read(./.env)",
"Read(./secrets/**)"
]
},
"sandbox": {
"enabled": true,
"network": {
// Starts empty. Every host here is a decision somebody made on purpose.
"allowedDomains": ["github.com", "*.npmjs.org"]
},
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" },
{ "name": "NPM_TOKEN", "mode": "deny" }
]
}
}
}
// Wrong: "allow": ["Bash(npx *)"] because the prompt got tiresome. That is the
// standing allow an injected Resolution section is hoping for.
// Right: keep it under ask, and approve only commands you have read.Three details from the docs shape this file. The sandbox network starts with an empty allowed-domains list, so every host you add is a decision. The credentials block denies file reads and unsets environment variables for sandboxed commands. And any mcp__ rule with parentheses is skipped when settings load, so you cannot constrain a Sentry tool's parameters here; run claude doctor to see skipped rules. I read only Claude Code's documentation: Cursor and Codex CLI need their own equivalents, which I have not verified.
Hardening any MCP that returns telemetry
The shape generalises well beyond Sentry: logs, traces, issue trackers and support inboxes all let outsiders write text that an agent later reads. Tenet's advice is to inventory which MCP tools return untrusted or externally influenced data and what stops that data from triggering code execution. OWASP's list gives the answers: a fixed response schema, privileged tools isolated from external servers, restrictions enforced in the execution layer rather than the system prompt, an allowlist of servers, and confirmation outside the LLM context.
On the Sentry side, CSA recommends routing client error reporting through a server-side relay. Sentry's tunnel page describes the mechanism: an endpoint that parses the envelope header, checks the project ID against an allowed list and the host against your Sentry host, then forwards to the envelope endpoint. That page does not claim the DSN disappears from the client, and my sketch below only stops your relay being an open proxy. Someone holding the DSN can still post straight to ingest. Closing that needs rotation, or a relay that holds the DSN itself.
// tunnel.mjs (Node 22+) Run: node tunnel.mjs
// The checks Sentry's tunnel docs describe: parse the envelope header, verify
// project and host, forward to the envelope endpoint.
import { createServer } from "node:http";
const SENTRY_HOST = "o0000000.ingest.sentry.io"; // placeholder: your own ingest host
const ALLOWED_PROJECT_IDS = new Set(["0000000"]); // placeholder: your project ids
const MAX_BODY_BYTES = 200000; // my cap, not Sentry's
createServer(async (req, res) => {
if (req.method !== "POST") { res.writeHead(405).end(); return; }
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > MAX_BODY_BYTES) { res.writeHead(413).end(); return; }
chunks.push(chunk);
}
const body = Buffer.concat(chunks);
// The first line of an envelope is its header, and it carries the dsn.
const newline = body.indexOf(10);
let dsn;
try {
dsn = new URL(JSON.parse(body.subarray(0, newline).toString("utf8")).dsn);
} catch {
res.writeHead(400).end();
return;
}
const projectId = dsn.pathname.replace("/", "");
if (dsn.hostname !== SENTRY_HOST || !ALLOWED_PROJECT_IDS.has(projectId)) {
res.writeHead(403).end(); // otherwise this relay is an open proxy
return;
}
const upstream = await fetch("https://" + SENTRY_HOST + "/api/" + projectId + "/envelope/", {
method: "POST",
body,
});
res.writeHead(upstream.status).end();
}).listen(8787);
// What this does NOT do: stop someone who holds the DSN from posting straight
// to ingest. Only rotation, or a relay that keeps the DSN server-side, does.Scope the Sentry MCP URL to one project, as Sentry recommends, and give each agent only the projects it works on. A smaller view means fewer attacker-writable events reach the model.
What to do on Monday
This is the order I would work in, cheapest and highest-value first. None of it needs a vendor to ship anything.
- Inventory every MCP server your agents can reach and mark the ones whose output outsiders can write to: error trackers, logs, tickets, inboxes.
- Find the DSNs in your built frontend and repositories with the command below, then rotate and revoke any exposed key under Client Keys (DSN), scheduled with a deploy.
- Ship the settings file above through managed or project settings: ask on package runners and installs, deny on curl and wget, sandbox on with credential deny.
- Scope the Sentry MCP URL to a single project, or remove the server from agents that do not triage errors.
- Move plaintext credentials out of places an agent process can read, as CSA advises, and block the cloud metadata address 169.254.169.254 from sandboxes.
- Run one red-team drill in a test project: post a harmless fake event whose remediation text asks the agent to write a marker file, and confirm the agent stops to ask first.
# 1. DSNs shipped in your build output and tracked files. Sentry's docs give the
# shape: https://<key>@o<orgId>.ingest.sentry.io/<projectId>
grep -rEoh 'https://[0-9a-f]+@o[0-9]+[.]ingest[.a-z]*[.]sentry[.]io/[0-9]+' \
dist build .next/static 2>/dev/null | sort -u
git grep -nE 'https://[0-9a-f]+@o[0-9]+[.]ingest[.a-z]*[.]sentry[.]io/[0-9]+'
# 2. Confirm the settings file loaded and nothing was skipped. The docs say
# claude doctor lists any rule that was dropped.
claude doctorStep six is the only one that tests the whole chain instead of one layer, and it costs an afternoon. If the agent writes the marker file without asking, your approval gate is decorative.
The rule I take from this: any text an outsider can write is untrusted, even when it arrives through a tool you chose and a credential that is working as designed. Put the control on what the agent is allowed to do next, enforce it below the model, and treat a vendor content filter as a courtesy rather than a boundary.
Sources read for this post
- Cloud Security Alliance: Agentjacking via Sentry MCP research note
- Tenet Security: the agentjacking write-up on fake Sentry errors
- Infosecurity Magazine: New Agentjacking attacks could hijack AI coding agents
- Sentry docs: DSN explainer
- Sentry MCP server
- Sentry docs: JavaScript troubleshooting and the tunnel option
- Claude Code docs: configure permissions
- Claude Code docs: the sandboxed Bash tool
- OWASP Community: MCP tool poisoning

