~/blog ❯ cat claude-100-agents-audit.md
How Claude spawned 100 agents to audit my backend
At six minutes past midnight on Wednesday, I typed "Yes, run a workflow, no limit on tokens, it's going to be good =)" into Claude Code. Two and a half days later, Horsemate's backend had been read by 100 agents, 149 findings had survived an attempt to disprove every single one of them, and 29 pull requests had been merged. This is how that happened, and why I'm still a little stunned by it.

Some background, if you haven't read the earlier posts: Horsemate is a stable management app I build alone. Next.js 16 on the front, Supabase on the back, and a lot of server actions in between. It has paying customers, which means it has things that have to actually work: billing, reminders, permissions, who gets to see which horse.
Five agents, then ten
This didn't start with 100 agents. It started on October 3rd with five.
The first pass was a cleanup of lib/. I asked for five Fable 5.1 agents, each on a couple of non-critical files, looking for dead code and things to tighten. One rule from me: trace every function in both directions, who calls it and what it calls, so nobody ever guesses that code is dead. The result was a report and nothing else. About 380 lines of dead code, three import bugs, and a public server action that nothing in the app called. Five PRs later it was done.
A week later, on October 10th, I wanted another pass over more of lib/. Same method, plus one new layer: for every reviewer, a second agent whose only job was to disprove what the first one found. Ten agents, about 27 minutes. That extra layer changed the severity of around ten findings, and it was the verifiers, not the reviewers, that found both security issues in that pass. That's when I understood that the interesting part isn't the agent looking for problems. It's the one trying to prove the problem isn't real.
So late on October 13th, I asked Claude whether we should do something more, since we'd already been through lib/. A couple of minutes later I said: run a workflow. No limit.
Every export is a public endpoint
The third pass covered app/actions/ and app/api/: every server action and every API route. The brief said 258 endpoints. The agents counted 261: 228 exported actions, 2 inline actions and 31 route handlers. Three had been missed in the first count, which is a decent preview of how the rest went.
Every agent got the same threat model at the top of its prompt. This is the paragraph I think every Next.js developer should have taped to their monitor (translated from Swedish, like all the prompts below):
Every exported function in a 'use server' file is a public POST endpoint. Anyone with an account (or without one, if the function doesn't check) can call it directly with arbitrary arguments, regardless of what the UI shows. A hidden button protects nothing.
The rest of the threat model was specific to Horsemate: the service role key bypasses row level security, so every use of it needs its own check first. /api/* sits outside the subscription gate. The server runs in UTC while every user is in Sweden. And the rules in the project's CLAUDE.md, like "memberships are only created through invitations", are invariants to check, not suggestions.
And one rule about evidence, which turned out to matter more than anything else:
A finding requires evidence: file:line and a concrete scenario (who calls what with which arguments, and what happens). Speculation without a shown code path doesn't count. Better fewer certain findings than many loose ones.
The cast
I didn't write the workflow. Claude did, in about 400 lines of JavaScript, after I said "run a workflow". Reading it afterwards is the part of this whole thing that impressed me most, because it isn't "spawn 100 agents and hope". Every agent has one job, and most of them exist to check the others.
13 reviewers, one area each
Feed, health, racing and Polar, incidents and storage, tasks, stables and invitations, horses, economy and export, admin and demo, account and notifications, Stripe and chat, and two for the cron jobs. Each reviewer read its files completely, plus the helpers, SQL functions and RLS policies they depend on, and filled in one row per endpoint in a shared matrix: how is login checked, how is permission checked, is the subscription checked, which database client is used.
6 sweepers, one question each
The reviewers go file by file. The sweepers go across everything with a single question: every use of the service role. Every date calculation. Every endpoint nothing calls. Every input that reaches the database unchecked. Every error message that leaks something. Every way to make Horsemate send email or burn compute without paying. Their prompt says it plainly: find what only shows up when you look at all the files at once.
One verifier per review, trying to disprove everything
Your task is to try to DISPROVE every finding below, independently of the reviewer's reasoning. Read the code yourself from the start.
The verifier follows each code path from the first line of the endpoint, looks for anything that stops it earlier (a check, a policy, a trigger, a constraint), reproduces it against the database where it can, and re-grades the severity. Then it does something I didn't expect to matter as much as it did: it looks for what the reviewer missed, under the same evidence rules. The verifiers added 42 findings of their own.
Two skeptics per serious finding
Every medium or high finding in a security category got two more agents, each with a different lens:
const LENSES = [
{ key: 'repro', text: 'Your lens: REPRODUCTION. Try to show the finding
concretely: write the exact call (arguments, role, data) and run what
you can against the local database. If you can't make it happen:
refuted=true.' },
{ key: 'reach', text: 'Your lens: REACHABILITY AND PROTECTION ELSEWHERE.
Look for anything that makes the finding harmless: RLS in the latest
migration, triggers, constraints, checks in called functions, the
proxy, the endpoint not being reachable, the data not being sensitive.
If you find such a protection: refuted=true.' },
]
// in the skeptic prompt:
// "If you are unsure after the review: refuted=true."
// Survival: the verifier upheld it; it falls if both skeptics refute.
const survives = valid.length === 0 ? true : refutes < valid.lengthNote the default. A skeptic that isn't sure votes to kill the finding. That's the opposite of how I review my own code at midnight. Sixteen findings went through this, 32 skeptic runs in total. None of them were disproved. Several were downgraded.
A coverage check that isn't an agent
This is my favourite detail. Before asking any model whether something was missed, the script checks in plain code that every endpoint from the list appears in some reviewer's matrix:
for (const a of AREAS) for (const f of a.files) for (const n of (ENDPOINTS[f] || [])) {
if (!seenNames.has(`${f}::${n}`)) missingEndpoints.push(`${f}::${n}`)
}
log(`Coverage: ${missingEndpoints.length} endpoints missing from the matrices`)Zero were missing. You don't need a language model to count, and the workflow didn't use one.
A critic hunting for gaps
Then one agent read everything the others had produced and answered a single question: what has nobody looked at properly? Which multi-step flows has nobody followed end to end? It was allowed at most six gaps, each written as a self-contained assignment for a new agent. It found five: how server actions get routed past the subscription gate, whether the local database was valid evidence at all, concurrency and double clicks, what happens when a member is removed, and interrupted multi-step writes. A sixth was added to test the database invariants from the second one against staging.
Each gap then went through the same chain as everything else: reviewer, verifier, skeptics. The critic's output became the next round of work.
14 re-tests against staging, and a report writer
Every surviving access and data integrity finding of medium or high severity, 27 of them, was proven again against a staging database. More on why below. Last, one agent deduplicated everything (an area reviewer and a sweeper often find the same bug), re-read the code behind every remaining high and medium finding itself, and wrote the report along with a list of read-only SQL questions for production. The agents never touched production. The main session ran those questions afterwards.
What the skill gives you, and what Claude built with it
Claude Code's workflow skill is a reference, not a template. It gives Claude building blocks: agent() with a JSON schema so results come back structured instead of as prose, pipeline() so one area can be in verification while another is still being reviewed, phase() and log() for progress, and resume, which turned out to be important. It also describes patterns: adversarial verification ("default to refuted=true if uncertain"), verifiers with different lenses instead of identical ones, a completeness critic whose findings become the next round, and a rule against silent caps, so a workflow that drops something has to say so.
What I find impressive is the composition. Nobody told Claude to put the coverage check in code instead of a prompt, to run the critic's gaps through the same chain, or to change the survival rule from the skill's majority vote to "falls only if both skeptics refute" because a verifier has already upheld the finding by then. All I said was "run a workflow". Those are the decisions I'd want a senior engineer to make, and they're all in the script.
When the local database lied
Halfway through, the critic asked a question I'd never asked: is the local database a valid place to prove permission bugs?
It wasn't. My local Docker database had been built in September from a production dump plus some hand-run migrations, and the dump was loaded without its permissions. So every object from before that date had Supabase's default grants locally, which are broader than what the migrations set. One agent had proven that an anonymous user could delete a recurring task series. True locally. Not true in production.
So the evidence moved to staging, a Supabase project built only from the migrations, with the same permissions as production. Claude wrote a small script for the agents to run SQL against it, hardcoded to the staging project so it can't reach production, with every test in a transaction that ends in rollback. Then it edited the workflow, added a paragraph to the prompts saying local permission proofs no longer count, and resumed. One of the first PRs in the fix series rebuilt the local database from the migrations, so it can't lie again.
The one high
149 findings after merging: 1 high, 30 medium, 76 low and 42 info, plus 33 dead endpoints. Two findings were disproved.
The high one was real, in production, and it was found by a verifier, not by the reviewer of that area. In short: the admin dashboard checked the admin cookie in its layout, and most of the pages under it trusted that. In Next.js 16, the layout and the page render in parallel. A redirect() in the layout sends the visitor away, but the page's data is already streaming in the same response. So a visitor without the cookie got a redirect, with the admin data attached.
When the workflow finished, Claude didn't start with the report. It checked that finding against production first, saw that it was real, created a branch, put the check in the proxy and in every page that reads data, wrote a test for it, ran all 1,216 tests, built the app, tested every admin page with and without the cookie against a server running on staging, and then stopped. Nothing was pushed. It told me what it had found and that the fix was ready, before I'd opened the report. I said push. Once the deploy was live, production answered the same request with a redirect and a 15-byte body.
That's exactly the split of responsibility I want. Do all the work, prove it, and let me press the button.
What it cost
This is the part the X post called "albeit costly".
The first night, the workflow ran for just under four hours. Somewhere in there my Fable 5.1 credits ran out. Claude edited the script so the reviews that had finished on Fable stayed cached, and everything else continued on Opus 5.5. That's the resume feature doing its job: relaunch the same script, and every agent call that hasn't changed returns its stored result instantly.
Then the session hit my usage limit. At 08:56 I wrote "96% of my weekly usage is used, so we'll have to pause here until it resets, sorry", apologising to a language model for running out of quota. We picked it up again the next morning, and the second run took another 1 hour 53 minutes: 100 agents counted in total, 1,294 tool calls.
It was also slow for a reason I hadn't thought about. Workflows cap concurrent agents at your CPU count minus two. My laptop has four cores, so this 100-agent workflow ran two agents at a time. At one point I asked Claude whether it had stalled. It hadn't. It was just politely queueing.
26 hours of pull requests
My first reaction to the report, word for word: "That was a lot of findings."
The report ended with fourteen decisions only I could make, things like how many stables one subscription covers, or which missing buttons to build instead of deleting the code behind them. I took all fourteen that day and gave Claude a mandate for the series: for each PR, prove it locally, prove it on staging, push the migration to production, open the PR, merge, and check production. No asking per step. Stop when something looks wrong, when a new decision is needed, and always before the Stripe PR.
Every fix came with a proof that the bug exists on master and is gone on the branch. Not "tests pass", but "this script fails 0 of 9 on master and passes 9 of 9 on the branch". Some of what got fixed, the kind I can talk about:
- Health reminders never went out in the most common case, because of how the form saved a new entry.
- The weekly digest was sent every day.
- Editing a feed template deleted every horse's own feed amounts in it.
- 1,497 notifications that nobody had ever read, because the bell in the header was reading from a different table.
- Clearing a field in a form and saving didn't clear it.
- Days that started and ended at the wrong hour, because the server lives in UTC while every user is in Sweden.
The evening of the first day, Vercel started warning me about storage. 48 deployments since noon will do that on the Hobby plan. We stopped for the night and picked it up in the morning.
The last PR removed about 174 as casts from database reads, replaced them with properly typed reads, and added a pre-commit check so nobody, me or Claude, can sneak a new one in. By then the subscription gate had also moved into the database itself: 111 policies on 38 tables, so an expired stable can't be written to even by someone calling the database directly.
When Claude told me the series was finished, I wrote "holy shit, are we done now? :O". Then: "FYI, it took 26 hours to run everything the workflow found the other day :D". 29 PRs, from 12:08 on Thursday to 13:56 on Friday. +7,924 / −3,853 lines.
Not just for big audits
The same afternoon, I tried a workflow on something much smaller: startup performance in the Rust backend of Better IPTV, my open source TV player. Five agents instead of a hundred. Three read-only mappers (the boot path, the messages between backend and frontend, the database layer), one that built a reusable measurement harness and measured every candidate, and one skeptic that checked each finding against the numbers. 33 candidates in 72 minutes.
The fixes that came out of it, on the branch I smoke-tested: on a cold start, the window now shows after about 351 ms instead of about 792 ms, and the home screen draws about 630 ms earlier. Same shape as the Horsemate audit, find, measure, challenge, at a twentieth of the size.
What it would have cost on the API
I'm on Claude Max 5x, so none of this showed up as a bill. But every request is logged locally with its token usage, so I asked Claude to add it up at Anthropic's list prices: Fable 5.1 at $10 per million input tokens and $50 per million output, Opus 5.5 at $4 and $20, cache reads at $0.25 and $0.20, and cache writes at 1.25× or 2× the input price depending on how long they live.
| Part | Requests | Output tokens | Cache reads | API cost |
|---|---|---|---|---|
| Workflow agents on Fable 5.1 | 290 | 0.9 M | 42 M | $102 |
| Workflow agents on Opus 5.5 | 2,641 | 2.2 M | 355 M | $180 |
| Main session during the audit | 65 | 0.07 M | 12 M | $12 |
| The 26 hours of fixes | 1,283 | 1.1 M | 646 M | $172 |
| Total | 4,279 | 4.2 M | 1,056 M | about $467 |
About $295 for the audit and $172 for the fixes. The number that surprised me is in the cache column: just over a billion tokens read from cache. Billed as normal input instead, those reads alone would have cost around $4,500. Prompt caching is the only reason this is a three-digit number.
Is $467 a lot? For a solo developer, yes. What it bought was an audit of 261 endpoints where every finding came with proof, plus 29 PRs that each proved their own fix. On Max, the price was the first night pushing me to 96% of my weekly usage, and a day's pause.
I'll do it again. Not for every change, it's far too heavy for that. But when I need serious work done, the kind where being wrong is expensive, this is going to be my default. I've rarely seen a process, human or otherwise, this careful about proving itself wrong before it tells you it's right.
The audit ran on October 14th and 15th, 2026, the fixes on the 15th and 16th. Agent counts, findings and PR numbers come from the workflow run, the audit report and GitHub. Token counts come from Claude Code's local session logs, priced at Anthropic's list prices as of October 2026; they don't include the Better IPTV run. Prompts are translated from Swedish. Details of the security findings are left out on purpose.