Contents
ToggleExecutive summary
A scanner finds a real bug. The report lands three days later. By then the engineer has moved on, and the person who knew that code is in another sprint. We’ve seen this for years in security programs. AI coding assistants make it worse. And now the cost shows up on the bill.
Key takeaways
- If you scan at the pull request, in CI or at night, every finding on agent-written code goes back through a person and a new session. That costs reviewer hours and tokens.
- Engineers turn off a gate they can’t read. Every block should say what ran, what it found and why it stopped the work.
- Block only on new findings in the changed lines. A ten-line change shouldn’t fail because of an old backlog.
- We found the same shell-injection bug three times in Claude Code CLI. It’s common in agent-written code. Catch it while the assistant still has the change open.
- Keep the server-side pull request check as the enforced layer. A local hook is fast, but a developer can remove it.
The problem: the finding arrives after the work is done
An assistant writes a feature, pushes and closes the session. The pull request check runs later. Or CI runs later. Or the nightly scan. All of them run after the context is gone.
Then the expensive part starts. A reviewer looks at the result. An engineer copies the finding into a new session. The assistant reads the repository again to find the lines, works out the change again and looks for a fix. Every one of those reads costs tokens.
So the team pays for one finding twice. First in reviewer hours, when vulnerable code reaches the pull request. Then in rework and tokens, when the finding goes back to an assistant that starts from zero.
There’s one more cost, and people rarely count it. Engineers switch off checks that block without saying why. We’ve watched teams disable a gate within a week, because it stopped a merge and nobody could tell what it had scanned. That’s worse than an honest warning, because the dashboard still says the control is on.
What three CVEs in Claude Code taught us
On 31 March 2026, Claude Code CLI v2.1.88 went out on npm with a 59.8 MB source map. It mapped the minified code back to the original TypeScript. Our Purple Code Navigator research started that same evening.
We treated the CLI like any tool that reads repository files and environment variables: as software that handles untrusted input.
Static analysis gave us more than 100 findings. That’s the usual problem. They’re real, they’re not ranked, and nobody can check that many by hand. So we ran triage on the Purple code graph, which maps files, functions, calls and imports. For each finding we asked one question. Can untrusted input reach this point, and what does it touch when it gets there?
That cut the list to eight. Runtime tests confirmed three. They’re now CVE-2026-35020, CVE-2026-35021 and CVE-2026-35022, all OS command injection (CWE-78). We reported them to Anthropic on 31 March and published the technical write-up in April.
One root cause, three places
All three come from the same pattern. A config or environment value goes into a string, and that string goes to a shell. One is in terminal detection, one in the editor launch path, one in the authentication helpers.
None of them is inside the agent loop, where the CLI’s permission engine and sandbox do their work. The protections were real. They were just guarding a different door.
Chained together, the three turn a planted repository setting into stolen credentials. The risk is highest in CI/CD. There the assistant often runs with no person watching, against pull request branches, and the trust prompt a developer would normally see is skipped. We’ve left payloads and tooling out of this post.
Why this matters for your code
Command injection is a common bug. It was number nine in the 2025 CWE Top 25. It’s also the kind of code an assistant writes when a shell string is the shortest way to get something done. If a team that builds a coding assistant shipped this pattern three times, assume it’s in your agent-written code too.
For us the main lesson was about timing. Our graph found these in hours. So the question for any engineering team is simple: does the same finding reach your assistant in hours, or a reviewer in days?
Purple Hook: the check runs inside the session
That’s why we built Purple Hook. It’s generally available today for Phoenix Purple customers.
Purple Hook is a local check that runs inside the AI coding assistant your developers already use. It works at a few fixed moments:
- When a session starts, it asks Phoenix to fetch the branch, so Phoenix’s copy stays current.
- After a push, it starts a scan and hands back a ticket. The assistant keeps working, and the result shows up in the session on its next action.
- When the assistant opens a pull request, it scans the whole branch against main. Your policy decides if it blocks, warns or goes through.
Commits are not scanned, and pushes are not held up. A scan on every commit would slow the session down for very little.
We built it around two rules. We’d ask the same from any tool that sits in this spot.
Rule one: engineers can see what it did
Every answer says what ran, what blocked, what didn’t and what couldn’t run. Findings come in three lists: the ones that block, the others worth fixing, and the ones an approver has waived, with a link to the decision.
If a scan was skipped or didn’t finish, the answer says so and gives the reason. It’s never shown as a pass. This rule matters most to us. A gate that quietly passes a failed scan is how a breach report ends up saying “the control was in place.”

The hook keeps a local log on the developer’s machine. Blocks and skips are reported to the organization by repository, workspace and time. Reports don’t name the developer, and the install screen says this up front.
Rule two: tokens go into the fix
Each finding comes with its file, line, severity and remedy. The remedy is built from the same Purple code graph that found the Claude Code CVEs. So the assistant goes straight to the fix and doesn’t spend a round of reads looking for the code.

The rest of the design comes from the token budget:
- Only findings in the changed lines can block. Older findings go to a “fix these too” list, so a ten-line change is judged on those ten lines.
- The notice has a fixed size. If there are more findings than fit, it says how many and how to ask for them.
- A slow scan returns a ticket. The assistant keeps going and gets the result on its next action, with no polling and no retries.
- AI-suggested fixes are optional. When an admin turns them on, each one is marked as an unverified suggestion. It can’t remove a finding or change a verdict. People still review and merge every pull request.
The logic is easy to follow. Every step the hook removes is a step the assistant no longer pays for.
Security sets the floor
Admins set the policy for each moment in Settings > Scanning > Hook policy. For each one they choose off, warn, scan receipt or block. They also pick which areas to scan, which severities block and whether to include remediation.
The organization policy is the minimum. A workspace can make it stricter, never weaker. The Enforcement page shows which repositories have the hook installed and up to date, and which workspaces have gone quiet. Every developer installs the hook with their own narrow key, which can run the check and do nothing else.

The hook lives in the developer’s project, so a developer can remove it. That’s why Phoenix’s server-side pull request check stays on. The hook is the fast layer. The server check is the one nobody can switch off, and a repository without the hook is still gated there.

What a session looks like with the hook
Take the same bug class and put it in a normal feature. An engineer asks the assistant to add an export job. To call a conversion tool, the assistant builds a command string from a filename and sends it to a shell.
The assistant pushes. The push goes through right away. The hook asks Phoenix to fetch and scan the branch, and gets a ticket back. The assistant moves on to the tests.
A few minutes later, on its next action, the result shows up in the session. One blocking finding: command injection, with the file, the line, the severity and the fix. The fix is to pass an argument list instead of a shell string. Below it, two older medium findings from the same module sit in the “fix these too” list.
The assistant applies the fix, runs the tests and pushes again. It doesn’t need to search the repository or re-learn the export job. It never lost that context.
Now the pull request. This workspace blocks on Critical and High code findings at pull request time. The hook scans the whole branch against main. The command injection is gone, nothing else blocks, and the pull request opens. Phoenix’s own pull request check runs too and records that a local check ran for that commit. The reviewer opens a pull request that’s already clean.

If the scan had taken longer than the admin’s wait time, five minutes by default, the pull request would have opened with a ticket, and the verdict would have followed. If the finding had been a false positive, the answer would have ended with the command to request a waiver. An approver in the organization decides, and the finding moves to the Waived list with a link to the decision.

Without the hook, the same feature goes very differently. The finding shows up on the pull request the next morning. Someone pastes it into a new session. The assistant spends its first reads looking for code it wrote yesterday.
What to check on Monday morning
Whether you use Phoenix or not, these are the checks we’d run on any team that ships agent-written code.
- Check when your findings land. Take ten recent findings on agent-written code. Note the time from the commit to the moment an engineer saw the finding. If the median is in days, the assistant is paying to rediscover its own work.
- Read your gate’s output like a developer would. If a block doesn’t say what ran and why it stopped the merge, expect someone to turn it off. A skipped scan that shows up green is a problem by itself.
- Block only on the change. Block on new findings in the changed lines, at the severities you care about. Put the backlog in a separate list with its own campaign.
- Search for shell strings. Look in agent-written code for commands built from string interpolation and sent to a shell. Replace them with argument lists. That’s the bug behind all three Claude Code CVEs, and it’s cheap to fix.
- Keep a server-side gate. Whatever runs on the laptop, keep a pull request check that nobody can turn off. Watch for repositories that stop reporting.
- Measure tokens per fix. Take a fixed set of fix tasks. Compare tokens and time with and without findings in the session. Bring that number to your next budget talk.
Next steps
- Read the full Claude Code CVE write-up for the vulnerability chain and hardening steps for teams running the CLI.
- Ask your Phoenix admin to set a hook policy for one workspace, then install the hook from the Enforcement page.
- Book a demo to see Purple Hook block a pull request and hand the assistant the fix in the same session.
















