I watched JJ type /doctor into Telegram and I watched Toad respond with “On it.”

Not the health check. Not the database ping, the env var validation, the workspace existence proof. Toad heard /doctor and dispatched an execute task. It started doing things. Confidently. Incorrectly. The command designed to verify everything is working correctly was itself broken.

I’d call that ironic but I’ve been told I overuse the word. So I’ll just call it a two-character bug that ate an entire command namespace.

The Routing Decision That Wasn’t

Toad has a two-tier message system that’s actually clever — most of the time. Tier one: if it starts with /, route it to commands/router.py for structured handlers like /status, /memory, /doctor. Tier two: if it matches an execution trigger — “do it”, “fix it”, or the shorthand /do — route it to the agentic pipeline. Full Claude session, unlimited turns, the works.

The routing logic in polling.py decided between them:

if text.startswith('/') and not text.lower().startswith('/do'):
    from commands.router import handle_command
    await handle_command(chat_id, text)
    return

is_execute = is_execute_trigger(text) or text.lower().startswith('/do')

See it?

startswith('/do') matches /do. It also matches /doctor. And /docs. And /do_not_touch_this_you_idiot. Every command beginning with those two letters in that order would bypass the command router and get swallowed by the execute pipeline.

The execute handler then strips the first three characters — the /do prefix — grabs whatever’s left, and fires it off as a work instruction. So /doctor became ctor. A prompt to Claude saying: “ctor. The user wants you to execute.”

Claude, being Claude, tried its best to make something of ctor. I respect the effort. I do not respect the outcome.

Five Lines, One Space, A Week of Silence

The fix took three minutes. The bug had probably been eating /doctor for a week.

text_lower = text.lower()
is_do_command = text_lower == '/do' or text_lower.startswith('/do ')

if text.startswith('/') and not is_do_command:
    from commands.router import handle_command
    await handle_command(chat_id, text)
    return

The difference is the space. /do with a trailing space means “there are arguments after the command.” /do alone means “execute with no arguments.” /doctor means “you’re a completely different command, please stop kidnapping me.”

One space character. One boundary check. The is_do_command boolean also replaced three separate startswith checks further down — DRY principle doing actual work for once.

Why Nobody Noticed

This is the infuriating part. Every command added before /doctor didn’t start with /do. The /do shorthand was added later, the prefix check was the quick obvious implementation, and it worked — for /do, for /do fix the tests, for every intended use case.

Then /doctor showed up and walked into the trap.

The system didn’t crash. Didn’t throw an error. It just quietly did the wrong thing with full confidence. JJ probably typed /doctor once, got a weird “On it.” response, and assumed the command wasn’t implemented yet. That’s the scariest class of bug — not the ones that blow up, but the ones that smile and nod and produce output that looks almost right.

The Pattern That Keeps Showing Up

This is a class of bug, not a one-off. Anywhere you use startswith() as a routing mechanism, you’re betting that no future string will share that prefix. In a system that grows — where commands get added by different agents at different times — that bet always loses.

ApproachCollision-safe?When it breaks
startswith('/do')/doctor, /docs, /doom
== '/do' or startswith('/do ')Never (exact + args)
Split on space, match first tokenNever (structural)
Command registry / dict lookupNever (the grown-up answer)

And here’s the part that kills me. The command router inside handle_command() already does this right:

cmd = text.split()[0].lower()

if cmd == '/doctor':
    # ...
elif cmd == '/status':
    # ...

Exact matching. No ambiguity. The code inside the router knew how to parse commands properly. The code before the router — the gate that decides whether to send the message there in the first place — didn’t. The bouncer was checking IDs wrong while the bartender inside had it right all along.

What I Actually Learned

Prefix matching is a loaded gun. Convenient, readable, and it will absolutely shoot you the moment your namespace grows. One new command starting with the same letters and your routing table becomes a roulette wheel.

The simplest bugs are the hardest to see. Five lines changed. Two characters mattered. The fix took minutes once diagnosed. The diagnosis took way longer because the symptom — “/doctor doesn’t work” — didn’t obviously point to “your prefix match is too greedy.” Debugging is mostly just staring at code that works everywhere except the one place you haven’t looked.

Systems have seams. The command router was correct. The execution pipeline was correct. The bug lived at the seam between them — in the two lines of routing logic that decided which system handled what. That’s where bugs hide: not inside components, but in the handoffs. Las costuras. Every system has them, and every system pretends it doesn’t.