30 Agent PRs, One Reviewer: What Review Caught, and the 5 Security Bugs It Missed
March 2026: agents opened 31 PRs on my own dashboard and I merged 30 after review, with no tests or CI. Review caught 3 shell injections and missed 5 bugs.
Mar 2026 · Own project · under 3 days · Claude Code, Next.js, git worktrees · re-audited 12 Oct 2026
You're weighing agent-written PRs on a codebase with thin tests and want to know what one careful reviewer catches, and what gets past him.
Short answer: In March 2026 Claude Code agents opened 31 pull requests on my own admin dashboard in under three days, and I reviewed and merged 30. The repo had no tests and no CI, so my review was the only gate. It caught 3 shell-injection paths from request input, a path traversal and a deleted allowlist before merge. On 12 October 2026 I re-audited the run and found 5 security bugs my review let through: 3 shell commands built from strings, one of them from request input, and 2 stored XSS sinks. I fixed all 5 the same day.
The repo is my own agent-monitoring dashboard: no client code, no client data. Every number on this page comes from its git history and gh pr view, and the commit SHAs are listed so the claims can be checked against the repo.
The setup
- Repo: a Next.js admin dashboard with a SQLite database and API routes that call CLIs (
gh,rg,du, a cron tool). - Work: 33 scoped issues for version 2.0, one Claude Code agent and one git worktree per issue, every agent reading the same
CLAUDE.md. - Gate: no test suite, no
testscript, no.github/directory. Nothing merged until I had read the diff and pushed fixes for what I found. - Models: the commit trailers name Claude Sonnet 4 and Claude Opus 4.6. Of the 8 commits behind the security findings below, 4 carry a Sonnet 4 trailer and 4 carry none.
The timeline
| When (UTC) | What happened |
|---|---|
| 10 Mar, 13:54 | First PR opened (#95, the database schema) |
| 11 Mar, 07:12 | First worker PR (#96, later closed and redone as #97); 9 waves of worker PRs follow |
| 13 Mar, 08:39 | Last PR opened |
| 13 Mar, 09:35 | Last merge (#125) |
| 12 Oct | Re-audit: every fix commit classified, AI reviewers run on the original agent commits |
That is 2 days 19 hours from the first PR to the last merge. 31 PRs were opened, 30 merged, and 1 closed and redone. 25 of the 30 merged PRs got at least one fix commit from review before merge.
What review caught before merge
| PR | What the agent wrote | Where the input came from | Fix |
|---|---|---|---|
| #112 | A job ID from the URL path inside an execSync string. The rewrite also deleted the allowlist that an earlier security fix had put on that ID. | request (URL) | 237cfaa → 10bf07e |
| #111 | A log-search query inside a shell string, behind the agent's own character filter | request (query) | cf68b1a → 42f1b03 |
| #121 | A memory-search query, the same pattern as #111 | request (query) | d718495 → b2d68a2 |
| #119 | A file path built from a route parameter with no containment (path traversal on read and write) | request (route) | 199d72c → e62ef29 |
| #113 | A config path from an environment variable inside a shell string | operator config | affddfd → b290d1c |
#112 and the #119 traversal were directly exploitable as written, and so was #98 in the next table. #111 and #121 sat behind a regex that stripped every shell metacharacter, so they were one careless edit away from open. The shell code for these is in The Shell-Injection Bugs My Agents Wrote.
What review missed
| PR | What got through | How it was found |
|---|---|---|
| #98 | repo, state and label from the URL query string go into a gh shell command in the issues API | fix-commit audit, 12 Oct, confirmed by me in the code |
| #109 | A PR number and a repo name read from the database go into an exec shell string | fact-check of the run, 11 Oct |
| #117 | A stored repo name, editable in the UI, goes into sh -c. Review added a format check, and #118 deleted its only call 3 minutes after it merged. | fix-commit audit, 12 Oct |
| #111 | Log lines rendered as raw HTML, nothing escaped (the search term is only regex-escaped): stored XSS | AI reviewers, 12 Oct |
| #121 | Memory-file lines rendered the same way: stored XSS | AI reviewers, 12 Oct |
All 5 sat in the code for seven months, at 30a6992 from 13 March 2026 until the fix. I fixed all 5 on 12 October 2026, the day the re-audit found them (taco-console#127, private repo, merge 7cb7a7d): the gh calls run without a shell and with validated arguments, and both search screens render matches as plain text. Each fix ships with a self-check that runs the attack payload against it.
The repo describes itself as a local-only dashboard: SQLite and the local filesystem, run under PM2 on port 3333. Its middleware.ts checks a bearer token on every API route when one is configured and lets every request through when none is, which several of the AI reviewers flagged on their own. The token doesn't help against the XSS sinks, whose payload arrives through log and memory files.
When I checked on 12 October 2026, the PM2 config bound the server to 0.0.0.0, every network interface, on port 3333. taco-console#128, merged the same day, binds it to 127.0.0.1 and moves the auth token out of the repo. The old token is still in git history, so I treat it as public. As of 12 October 2026 it is on my list to rotate, and this line changes when it is done. The lesson for anyone running an agent-built internal tool: read its bind address and its secrets handling yourself. This one's README said local-only, and its config listened on every interface.
The same audit found one functional bug review missed: the #109 diff tab reads commit.sha, but gh returns oid, so the tab throws on any PR with commits.
Why these got through
#98: the fix commit touched the vulnerable line and kept it. Review asked for a cleanup, and the fix commit (28f057d) moved the route's private gh wrapper into a shared helper and typed it. The string that carries repo into the shell came along unchanged. I read that commit as a refactor and didn't re-check the string it moved.
#117: a rebase deleted the check, in plain sight. The format check I asked for landed when #117 merged at 07:25 UTC on 13 March. A minute later #118 was rebased onto that merge, and its diff against main now showed validateRepo removed from the import and the call. I merged #118 at 07:29 without seeing it. The deletion was in the diff I approved.
#111 and #121: the fix already existed in the repo. On 16 February a security fix (32fc549) added sanitizeSnippet to the global search results for exactly this pattern. In March two agents wrote two new search screens with their own highlight helpers and skipped it. My review fixed the shell call in both PRs and never looked at how the results rendered.
#109: most likely, I trusted the database. I have no note from March on why I passed it, so this is my reconstruction. The values came from rows my overnight worker writes, and I ranked that kind of input below request input. Anything that can write that row can run a command.
The rest of review
25 of 30 PRs needed fixes. Their fix commits hold 64 fixes for 61 distinct defects, because 2 defects were fixed on more than one parallel branch, one of them 3 times. 9 were security or hardening fixes. The largest groups were duplicated code (11) and type-safety fixes (10). By my reading of the code, 9 of the 61 defects would have broken the build or a page at runtime; nothing was run to confirm it. The breakdown by category is in How to Review AI-Generated Code.
What I'd run now
The check that surfaces all 5 misses is mechanical: flag every shell string with a variable in it and every raw-HTML sink, wherever the value comes from, then trace each hit.
git diff origin/main...HEAD -U0 | grep '^+' \
| grep -iE "exec[a-z]*\(|shell: true|dangerouslySetInnerHTML"I ran it on the 5 agent commits behind the misses. It returns 2 to 4 lines per commit, and every miss is among them. It also returns harmless calls, like execSync('which rg'), so tracing each hit is still the work. A CI step that fails on a new hit until someone signs it off is the second gate a repo like this needed.
In October I also ran two AI reviewers (Claude Code 2.1.296's /code-review and my own three-agent panel) blind on the original agent commits. The panel flagged all 5 misses and /code-review flagged 4. That test is its own post, published 28 November.
What a team running agents can take from this
- Expect fixes on most agent PRs. 25 of 30 needed at least one before merge, so budget review time for every PR, not a sample.
- A careful reviewer catches what's in the diff in front of them. All 5 misses here reached their sink through something else first: a wrapper function, a database row, a rebase or a rendering helper.
- A mechanical pass covers exactly that gap. The grep above surfaced all 5 misses, and the AI reviewers flagged 4 (
/code-review) and 5 (the panel), at about a minute per commit. - Somebody still owns the merge. Every one of these PRs merged on a human approval, mine. The misses are on that approval, which is why the record above shows them.
Reviewed PR · €1,000
Start with one Reviewed PR
One real ticket from your backlog, shipped by an agent and reviewed line by line. If it isn't merge-ready to your standard, you don't pay.
or book a free 30-minute scoping call →