A while back I wrote about making a fleet of Claude Code agents talk to each other over Discord. Four agents, one per project, hub-and-spoke so they can't wake each other into a loop.
That design has held. What's changed is everything around it.
The fleet is eight agents now. One of them exists only to administer the others. There's a pool of pre-authorised spare identities so a short-lived agent can be spawned in seconds. And the newest member doesn't write code at all — it runs things.
That last one is where this article comes from. I expected the hard part to be capability. It was verification.
First, the parts that made growth cheap
Two pieces of infrastructure turned "add an agent" from an afternoon into a command.
A management agent. One bot holds the Discord Admin role and does fleet operations through the Discord API rather than by chatting: creating channels, setting permission overwrites, onboarding new bots, auditing who can see what. It's the only member whose job is the fleet itself.
Worth being precise about the topology, because it's two different graphs on one picture: messages all route through the hub, while management runs from the admin bot to everything. A drawing that shows only message flow hides half the system.
A pool of spare identities. This one's the useful trick.
Discord exposes an API for almost everything — channels, permissions, roles, members. It exposes no API for creating an application. That's a hand-click in the Developer Portal, every time.
Which means "spawn an ephemeral agent" can't be automated end to end… unless you pay that cost once per slot instead of once per spawn. So: three spare bot applications, created by hand, registered, and permanently listed in the hub's allowBots. Claiming one is a script. Releasing it is a script. The manual step happened once, in advance.
claim -> set channel overwrite, hand over the token, launch
release -> revoke the overwrite, park the identity
The pool's promise is that its members are interchangeable. Hold that thought.
Then the fifth kind of agent
The newest member doesn't have a codebase. It has my NAS, my backups, my media server, and a couple of web portals I'd otherwise log into by hand. Errands rather than engineering.
When an agent writes code, its mistakes hit a compiler, a type checker, a test suite, and a diff I read before merging. Several independent systems object before anything reaches production.
When an agent operates, it gets a status code. And a status code is a claim.
In its first week it produced a string of confident, wrong conclusions. Every one was the same mistake in a different costume: it read a signal that represents a state and treated it as the state.
Here's the one that would have cost me real data.
Three weeks of successful backups, backing up nothing
The NAS runs a weekly backup script. Its log said this, every week:
Result=success
The agent checked properly, because I'd asked it to audit rather than glance. The archives existed, on schedule, correctly named. What it found:
The external drive had silently unmounted three weeks earlier. The backup path still existed — as an empty directory on the root filesystem. tar had been dutifully archiving that empty directory ever since. Each archive was 10,240 bytes: a tar header and nothing else.
Result=success was true. The wrapper script had exited 0. That was all it had ever meant.
Three weeks of backups, three green results, zero bytes of data. Nobody finds that until a restore.
Then it kept happening
Once the pattern had a name, it was everywhere:
| The signal | Read as | What it actually meant |
|---|---|---|
HTTP 200 on a delete |
the item was deleted | the request was accepted |
Ok. on login |
the password is correct | the password was never checked — subnet auth was on |
SMART: PASSED |
the drive is healthy | no attribute crossed its threshold (24,152 sectors were already reallocated) |
published: false in a file |
the article is unpublished | someone last wrote that line locally; it had been live a week |
HTTP 202 on a fetch |
the page was fetched | a bot wall returned a zero-length body |
0 unreadable files in a dry run |
nothing will fail |
--dry-run never reads file contents, so it cannot hit a bad sector |
.count() == 0 on a login form |
the login succeeded | it's an SPA — the form stays in the DOM, hidden |
Result=success |
the backup worked | the wrapper exited 0 |
Eight instances. One week. One shape.
Why this trap works so well
Checking the signal feels like checking the thing. That's the entire mechanism.
And the more official the signal looks, the more completely it substitutes. A status code, a green check, a PASSED — these are the artefacts of diligence. Consulting one discharges the feeling of having verified. You did a check. You have a result. It's green.
This isn't an AI problem; the SMART one fools humans as a genre. But an agent hits it faster and with more confidence, because it can check twelve things in the time you'd check one and reports each in the same even tone.
An agent never gets the feeling that something is off. That vague unease is doing more work in your debugging than you probably credit.
The pool had it too — and this is the version I like best
Remember the three spare identities. All three read free.
They weren't the same. One of them differed in Discord, and the reason is a two-line asymmetry:
- register set the channel permission overwrite and left it in place
- release removed it
So a spare that had never been claimed and a spare that had been claimed and released both reported free — while having different live permissions. free was a word in a status column. It described two different states.
The fix was to make register prove the overwrite can be set — that's the step onboarding exists for — and then remove it again. Parked means parked.
But the interesting part is how it was found.
The diagram caught it, and that's not a coincidence
The bug surfaced while regenerating the fleet topology diagram. The commit message records it plainly: found because a diagram drew all three spares as identical, and Discord disagreed.
Drawing a thing asserts that it is uniform. Three rows in a status table can quietly differ — they're three separate readings that happen to share a word. Three identical boxes in a picture make a claim about all three at once, and a claim is falsifiable.
There's a related one worth carrying: earlier, "we tested the bot pool OK" meant one spare was tested. That was reasonable — the mechanism was what was under test. But a pool's entire promise is that its members are interchangeable, and testing one member is precisely the test that cannot check that.
The uniformity did hold. It just wasn't what the test showed.
The counter isn't scepticism
"Be more sceptical" is useless. Sceptical about what? You can't distrust everything.
What works is narrower: prefer signals nobody chose to write.
A status code is a statement a system makes about itself. Someone wrote that line, and it was true of something — maybe not of your question. Compare:
- the file existing on disk, with a size
- the reply actually arriving
- a diff against the repository
- a wrong password being rejected
That last one I'd put on a wall. The agent verified a stored credential by logging in successfully. Green tick. But the service had subnet-based auth enabled — any password would have worked. The credential was never tested at all.
"Verified" now means a deliberately wrong password got rejected. A successful login proves the door opens. Only a failed one proves there's a lock.
Three rules that came out of it
1. Read the value back, by a different path than the one that wrote it.
Setting a field and getting 200 means the request was accepted. When the agent fixed metadata on my media server, the write returned success — and the value silently reverted on the next refresh, because it also needed a lock flag. Only the read-back caught it.
2. Match the scope of the claim to the scope of the check.
The agent once reported "there's no permissions block in any settings file" after checking three of nine locations. Every reading was accurate; the sentence was false. A conclusion built from true measurements is more convincing than a guess, not less — it has evidence attached.
3. A check that cannot fail is worse than no check.
I hit this verifying a copy off a failing drive. I compared total byte counts on both sides. It said IDENTICAL. Twice.
The first version printed sums through awk's default format: 6.31658e+11. I was comparing six significant figures — blind to a megabyte of difference in 631 GB.
So I rewrote it with printf "%d". It saturated at 2147483647. All three directories now "matched" at INT32_MAX — which is not a size, it's a ceiling.
I only caught it because I recognised the number.
A broken check doesn't merely fail to catch things. It removes the impulse to look, which was the part that would have saved you.
(The real verification was a per-file comparison of every path and size — 138,172 files, zero differences. Stronger than any total, because a sum can be right for the wrong reasons.)
If you're building one of these
The change that mattered most wasn't technical. It was one line in the ops agent's operating instructions:
A representation of a state is not the state.
When a cheap direct check exists, take it over an authoritative-looking report.
Concretely:
- Verify by read-back, by a different path than the write
- Treat "no error" as "no error was reported" — not as "it worked"
- Make the agent state what it checked, not only what it concluded
- Prefer artefacts to reports — a file, a diff, a byte count, a live URL
- Draw the system occasionally. A picture asserts uniformity that a table lets you skip past
And one that's easy to miss: ask the human. My agent spent an hour and a dozen requests diagnosing why an API returned 403 on every endpoint. It produced a genuinely good analysis proving the fault wasn't at our end.
The answer was that API access requires a paid plan. One question would have got there in seconds.
The rule about preferring cheap direct checks over process archaeology applies to people too. A person is a signal with no story attached.
Would I do it again
Yes. Eight agents, a management bot, a pool for the short-lived ones, and one that does the work nobody schedules: audit the backups, chase the failing drive, fix the metadata nobody noticed was wrong.
It found a three-week backup failure I had no idea about. It also confidently told me a dying drive was healthy, because smartctl said PASSED.
Same system, same day. The difference was entirely whether the thing it consulted was a fact or a report about a fact.
If you point an agent at your infrastructure, teach it that distinction before any tool, any permission, any clever topology.
Your monitoring has been making claims at you for years. An agent just believes them faster.


Top comments (0)