The thing that kept happening
Someone asks in chat: "booking BK-xxxx failed, can you check?"
The answer is a fifteen-step procedure. Find which log source has that identifier — there are four, and which one depends on what kind of id you were handed. Turn the VPN on. Discover the database still times out because a route bound itself to the wrong interface. Fix the route. Query the booking row, then the booking log, then the webhook log. Decide whether the supplier broke or we did.
Everyone on the team could do it. Nobody could do it quickly, because the procedure lived in the head of whoever had done it most recently — and the traps lived nowhere at all. Each new person paid full price for the same half-day.
We had documentation. Nobody opened it. Documentation you have to remember to read loses to a colleague you can just ask.
The realisation
A slash command in Claude Code is a markdown file. The body of the file is the prompt. That is the whole mechanism.
Which means the runbook and the tool are not two artifacts that drift apart — they are one file. Writing the procedure down is shipping the automation. There is no second step where someone converts the wiki page into a script and then the wiki page goes stale.
So the documentation problem became a distribution problem, and distribution I knew how to solve.
What I built
Two packs, one at each company:
- ~24 commands for the gateway team — reproduce a failure locally, trace any identifier to a timeline, rebuild the exact upstream request from a trace-id, provision or clone a supplier, run a supplier's certification suite, ship.
- ~18 commands for the other — debug one service end to end, check whether a multi-repo feature actually landed in every repo, reconcile an insurance order against its flight legs, build UI without inventing a second colour system, write the daily worklog from git plus tickets plus chat.
Deliberately two packs and not one. Different infrastructure, different integrations, different traps. They share a shape, not content.
The four decisions that made it work
1. Every command declares its blast radius, in the index.
🟢 read-only 🟠 writes to the production DB 🔴 deploys to productionThe risk of handing an agent a shell is not that it does the wrong thing. It is that you cannot tell at a glance which of these forty things is dangerous. Put the marker in the table of contents, not three paragraphs into the body, and the reader is warned before they are curious.
2. Anything that writes follows one protocol, and the protocol lives in the tool.
Dry run → print the plan → get confirmation → apply → re-read and verify.
Written into the conventions is the sentence I care about most:
A command that skips a step is a bug in the command — fix the command rather than working around it by hand.
That single line is what stops the protocol decaying. Once "just this once" is a bug report instead of a shortcut, the guardrail survives contact with a bad day.
3. Never assume where a connection points — make it prove it.
Local config files differ per machine: production, staging, a read replica, a
port-forward. So before any write, the command runs SELECT @@hostname, @@read_only and shows the result to the human.
This is not theoretical. On the other side, the trap sitting at number one in the
README is: several services commit a local config pointing at production,
with the test values commented out. Clone, npm run start:dev, and your laptop
is writing to the production database with no warning at all. You identify it by
the database name — the test and production names differ by a suffix.
4. Nothing personal is hardcoded.
No /Users/... paths, no personal emails, no user ids, no internal IPs.
Everything goes through an environment variable, with a table saying what breaks
if you leave it unset. Then a grep gate before commit:
grep -rn "/Users/\|@gmail\|password.*=.*['\"]" .claude/commands/ # must be emptyA default that is silently wrong is worse than making someone type the value in.
Two smaller things that mattered more than expected
Install by symlink, so git pull is the update. Adoption does not die at
install, it dies at upgrade. If getting the new version means re-running an
installer, the team is running last month's commands within a month.
Set the security bar by who can read the repo, not by whether it is "internal". One of the packs lives in a repository that anyone on the company network can read without logging in and without being added as a member. That is a much wider audience than "private repo", so the bar is higher: no credentials, no real hosts, no ports, no customer data — including in examples. Write down why the bar is where it is, or the next contributor will reasonably assume "internal" means "safe".
And the contribution rule that keeps the packs alive:
Lost more than 30 minutes to a trap? Add it to
docs/, with the verbatim error message.
Verbatim, because the next person will not search for your tidy summary. They will paste the exact string the machine printed at them.
What I took away
The best format for a runbook is one the computer can execute. Not because the automation saves the typing — it does, but that is the small win. It is that a document that gets run cannot quietly go stale. It breaks, loudly, the same day the system changes.
Label danger where the eye lands first. A warning buried in the body of a document is a warning for people who already read carefully. The people you need to protect are the ones scanning the index at 11pm.
Put the guardrail in the tool, not in the human. "Remember to dry-run first"
is not a safety mechanism, it is a wish. --dry by default, a confirmation gate,
and a verification read afterwards — those are mechanisms, and they work when
the human is tired.
Write down what you were unsure about. Both packs require a "what I am not certain of" section in their audit output. It costs one paragraph and it is the difference between a document a colleague can build on and a document they have to re-verify from scratch.