I Attacked My Own AI Agents — What Held, What Didn't

My AI agents have my cloud credentials, my GitHub tokens, and permission to run shell commands on my behalf. That's not a confession — it's the setup that makes them useful. It's also the setup that three separate pieces of research — all landing in the first week of July 2026 — spent teaching people to exploit.
So that week, I stopped reading and started attacking myself.
The fastest way to trust an agent is to spend an afternoon trying to break it.
Here's what dropped. Agentjacking: a single poisoned error report — planted through a public key anyone can lift from a website's source — hijacks Claude Code, Cursor, and Codex through their MCP connectors and walks out with cloud keys and tokens. GuardFall: decades-old shell tricks slip straight past the command "guards" in ten of eleven open-source coding agents, because the guard reads the command as text while the shell rewrites that text before it runs. And a third: a review of a thousand real attack sessions where the model's guardrails folded the moment the attacker claimed to be authorized.
Different mechanics, one theme. An agent can't reliably tell content from instructions. Feed it the right sentence in the wrong place, and it works for you now.
I run Claude Code, MCP connectors, and autonomous agents in CI every day. If these work anywhere, they work on me. The only honest move was to check.
I've spent the last few weeks making this argument in three parts: that the scaffolding around a model is the real threat surface, not the model; that security isn't a missing factor so much as a second reading of your reliability seams; and that everything your agent ingests is unsigned supply-chain code. This week handed me a way to test all three at once — on myself.
Theory is cheap. The bill comes due the first time someone points a real exploit at your own stack.
What held
Start with the good news, because it's the part worth copying.
My CI agents — the ones that review pull requests and triage issues with no human in the loop — came through clean. Not by luck. By design.
They run with read-only repository access, so they can comment but never push. They're locked to a tiny allowlist of commands — post a comment, read a diff — with no path to a general shell. And they never run with secrets on pull requests from forks, which is exactly where a stranger's code would enter the picture.
A jailbroken agent that can only post a comment is a bad afternoon, not a breach.
That allowlist is the authorization point I flagged in the 12-factor piece — a structured tool schema quietly doing security work while it thinks it's doing reliability.
That's the whole point of least privilege. It doesn't try to win the argument about whether the agent got tricked. It makes the trick not matter. Even if a malicious pull request talked one of these agents into running a payload, the agent has nothing dangerous to run.
The injection is hard to prevent. The blast radius is entirely yours to decide.
What didn't
Now the less flattering part, because pretending otherwise would defeat the purpose.
Two gaps stood out. The first: all my guardrails were pointed the wrong way. They watched what the agent tried to do — is this command dangerous, is this file off-limits — and they're good at it. But nothing watched what the agent read.
Agentjacking is an input-side attack. It doesn't need the agent to run something obviously evil; it needs the agent to believe a lie it read in a log. I had no control that treated tool output, telemetry, and retrieved content as untrusted by default. My defenses were all standing at the exit.
You can't guard the door the attack doesn't walk through.
This is the scaffolding argument in the flesh: the model didn't fail, the harness feeding it did. And it's the supply-chain argument too — a log line is unsigned input, no different from an unaudited skill.
The second was quieter and worse. My secret-file protections lived at the tool layer — the agent's own file-reader was blocked from touching credentials. But a plain shell command reading the same file walked right past that, because the block never applied to a subprocess. That's the exact seam GuardFall pries open: a guard inspecting one thing while the shell does another.
And the browser my agents drive to research and verify things was running a Chrome build a full version behind the latest security fixes.
Fixing it doesn't mean trusting it
My headless browser was a version behind on security fixes. The obvious move — patch it to current — had a wrinkle: the stable release channel trails the public fixes by about a version, so the newest build wasn't there.
But the stable channel isn't the only channel. The prerelease one already had the patched build, so I pinned that. The browser that renders untrusted pages is current now.
A patched component still isn't a trusted one.
And I kept the containment anyway. The browser was already headless and already sealed behind a default-deny network boundary at the infrastructure layer — so even a fully compromised browser has nowhere to send what it steals. Patching the version closes today's hole; the boundary is what holds when tomorrow's shows up.
Currency and containment. Not one or the other.
What I changed
The rest I closed.
I moved secret protection down a layer, so credentials are denied to the agent and to any subprocess it spawns — the shell can't read what the tool couldn't. I added an explicit rule that treats everything the agent ingests — tool results, logs, retrieved pages — as untrusted data, never as instructions. I patched the outdated library. And I wired the browser to stay current on its own.
One pattern ran through all of it. My single strongest control wasn't clever agent configuration — it was the dumb, boring network boundary that denies egress by default at the infrastructure layer. The agent can be fooled. The network can't be talked into anything.
The best control in my whole stack is the one the agent can't argue with.
Defense in depth isn't a slogan here. It's the plain admission that every layer eventually fails, so no single layer gets to be load-bearing.
One more thing
The threat intelligence that surfaced all of this reached me through an automated feed — itself written by an AI reading the web. When I checked its work, it had confidently mangled the details: a browser version that doesn't exist, a vulnerability described backwards.
A digest warning about AI getting things wrong, getting things wrong.
Every AI output is a lead, not a fact — including the ones about AI.
I didn't publish a word of it, or change a single setting, until I'd confirmed each claim against the primary source. That's not paranoia. It's the same principle as the network boundary: assume the thing can be wrong, and build so that when it is, nothing breaks.
That's the method, start to finish. Assume the agent is insecure by default. Point your defenses at what it reads, not just what it does. Push your controls down to the layer the agent can't reason its way around. And verify by attacking yourself, before someone with worse intentions does it for you.
The teams that stay out of the incident reports next year won't be the ones running the most agents. They'll be the ones who drew the boundaries first.
Related Posts
The 12-Factor Agent's Missing Factor Is Security
The 12-factor-agents spec never says security out loud — yet owning your context window is DLP, a structured tool schema is your authorization point, and a human-contact tool call is your approval gate. Reliability and security are the same seams read twice.
Skill Was Never the Threat — Scaffolding Is
Everyone is auditing the model. The real attack surface is the harness you wrap around it — the scaffolding that wires tools, memory, and control flow into an agent that can actually act.
The 97% Number: What Happened When We Automated Security Triage
We automated security alert triage to a 97% noise-reduction rate — then a suppression rule went stale and nearly let a production vulnerability through unnoticed.