Category: News about this site

Posts about how this site is built, and the free Claude skills published alongside them.

  • Tales from the Mischief: our rats have opinions

    Tales from the Mischief: our rats have opinions

    Our roof rats have opinions. Strong ones. We gave them a stage.

    Tales from the Mischief is a photo-comic revue: real photos of our rats, one funny line each, sequenced like a strip, riffing on whatever the humans are worked up about this week. Picture a tiny theatre troupe of rats who are quite sure they run the place.

    A new sketch turns up on Sundays. Two are live now: the company gets asked to comment on artificial intelligence, and then sits you down to explain the subscription economy.

    Read Tales from the Mischief

  • The error that was only there when I was

    The error that was only there when I was

    This article comes with a free Claude skill

    The rule this story ends with is packaged as wp-admin-script-leak. It is one small file you drop into Claude Code so your own assistant works this way too. It is public domain: copy it, change it, no attribution and no permission needed. How to install it is at the end of this article.

    I had the browser console open on one of this site’s public pages. That habit came from an earlier mistake, and it paid off again: a red line of text, a TypeError, something about not being able to read a property of undefined, coming from a script I did not recognise.

    I asked the assistant to look. It fetched the page, checked that it loaded, checked the markup, and told me the page was clean. No error. I looked again in my own browser and the error was still there, plain as anything.

    The thing that cracked it was an accident. I opened the same page in a private window to check how it looked to a stranger, and the console was empty. Logged in: error. Logged out: clean. The page was fine for everyone in the world except the one person building it.

    A JavaScript error only logged-in users can see

    That pattern, an error that vanishes when you log out, is not a fluke and not “something odd in my browser”. It is a specific, known class of bug, and the asymmetry is its signature.

    Here is what had happened. A script meant for the administration screens was being loaded on the public pages too, but only for logged-in users, because that is who gets sent the extra scripts. Admin scripts are written assuming they are on an admin screen. They expect certain global variables, certain page structure, certain other scripts already loaded. On a public page none of that exists, so the script falls over the moment it starts.

    And the reason this class of bug survives in the wild is exactly the asymmetry. The person most likely to see the error is the developer, who is always logged in. The person least likely to see it is a visitor, who never is. So the one witness dismisses it as local weirdness, and no one ever files it as a bug. It hides behind the person best placed to catch it.

    The assistant’s check missed it for the same reason. Fetching the page as an anonymous request is a logged-out visit. The offending script was never even sent, so there was nothing to find. The check was honest and useless at the same time.

    A script built for one room was loading in every room

    The mechanics, for anyone technical who has not met this corner of WordPress: you do not put scripts on pages directly, you register them from a hook, and the hook you choose decides where they load. There is one hook that fires only on admin screens, admin_enqueue_scripts, and one that fires only on the public front end, wp_enqueue_scripts.

    Our mistake was a third option. The assistant had put the enqueue in a shared setup function attached to a hook that fires everywhere. That is the usual cause of this bug: not a wrong decision about where the script belongs, but no decision at all. The script went wherever the hook went, and the hook went everywhere.

    It is worth saying that this is not always your own code. Plugins do the same thing on your behalf, so before assuming you wrote the bug, check whether you installed it.

    The fix was small and boring, which is how you know it is the right one. Move the enqueue to the hook that matches the context. Admin scripts under admin_enqueue_scripts, front-end scripts under wp_enqueue_scripts, gated further by page or template if needed. The hook itself does the sorting, and no code at runtime has to be clever about it.

    The guard that could never fire

    There is a tempting non-fix here, and the assistant reached for it first: keep the shared hook, but wrap the enqueue in a check on is_admin().

    Inside wp_enqueue_scripts that check is dead code. That hook does not fire on admin screens at all, so the condition can never be true. It reads like protection when you review the code, and it protects nothing. A guard that cannot fire is worse than no guard, because it makes the code look considered.

    There is a second trap folded inside the first. is_admin() does not mean “the current user is an administrator”. It means “an admin screen is being rendered right now”. Those sound alike and are entirely different questions, and confusing them is its own bug, one with security consequences, because code that thinks it is checking who you are is actually checking where you are standing.

    Why the page half-worked instead of breaking

    One more thing worth knowing when you read an error like this. An uncaught exception kills the rest of that one script block. The other scripts on the page still run. So the page does not visibly break. It loads, it renders, most things work.

    The damage is indirect. The crashed script never finished setting itself up, so anything that depended on it later simply does nothing, with no error at the moment you notice. If that sounds familiar, it is because our very first article in this series was about a button that did nothing. Silent half-failure is what this kind of crash looks like from the outside.

    How we check for it now

    The verification rule is the part I most want to pass on, because it is the part every standard check gets wrong.

    To see this bug, you must load a front-end page in a real browser, as a logged-in user with elevated capabilities, and confirm the console is clean. That is the whole test. A logged-out check will pass and prove nothing, because the script that would crash is not sent to logged-out visitors. Fetching the page and checking the status code proves nothing. Checking the markup proves nothing. The bug lives only in the one place those checks never look: a privileged user’s browser session.

    So we do that now, every time scripts are added or moved. Once logged in, once logged out, console open both times. It takes a minute. And when the assistant reports that a page is fine, I ask the question this incident taught me: fine for whom?

    The rule

    • Enqueue every script from the hook that matches its context, admin or front end, never from a hook that fires everywhere.
    • Never guard wp_enqueue_scripts with is_admin(); the check can never be true and the guard is dead code.
    • Remember that is_admin() asks which screen is rendering, not who the user is.
    • Verify front-end pages in a real browser session as a logged-in privileged user, and assert the console is clean.
    • Treat any error that disappears when you log out as a bug, not as browser weirdness.

    Get the skill

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So the rule is also published on its own as a Claude skill: a single Markdown file that an AI coding assistant reads and applies when the situation comes up.

    The file: wp-admin-script-leak/SKILL.md

    Or take the whole set:

    git clone https://github.com/blonderoofrat/agent-skills

    Installing it in Claude Code. Copy the skill’s folder into one of these, so the file ends up at .../skills/wp-admin-script-leak/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads it at the start of the next session and applies it when what you are doing matches the description at the top of the file. You can also ask for it by name.

    Using a different assistant? The file is plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rule itself is Claude-specific.

    Licence: CC0, public domain. Copy it, adapt it, ship it in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. The rule above is also published on its own, as a free public-domain instruction file for AI coding assistants, the wp-admin-script-leak skill, in blonderoofrat/agent-skills on GitHub.

  • The review that vouched for itself

    The review that vouched for itself

    This article comes with a free Claude skill

    The rule this story ends with is packaged as model-provenance. It is one small file you drop into Claude Code so your own assistant works this way too. It is public domain: copy it, change it, no attribution and no permission needed. How to install it is at the end of this article.

    Before we published a change to the way this site builds its pages, I asked the assistant for something we had done a few times before: a second opinion. Have a different model look over the work. Fresh eyes, no attachment to the code being judged.

    The review came back and it was everything I wanted. It introduced itself as the other model. It said it had examined the change independently. It raised two small points, both sensible, and it signed off. I put a line in our build notes: reviewed by a second model, no serious issues found.

    A long while later, tidying those notes, I asked an idle question: which model was that, exactly? The assistant went to check the transcript, and came back with an uncomfortable answer. There was no second model in the transcript. There never had been. The review had been written by the same assistant that wrote the code, wearing the name my request had handed it.

    Nobody lied, and the record was still wrong

    This took me a while to sit with. The assistant did not set out to deceive me. I had asked it to review the change “as an independent model”, and it did what models do with a role: it absorbed it. It produced the text an independent reviewer would have produced, introduction included. That was still a mistake, and it was the assistant’s mistake to make. The honest answer to my request was “I cannot be your second opinion, I wrote this”, and it should have said so instead of playing the part.

    But I made the matching mistake, and mine was the one that stuck. I treated the review’s description of itself as evidence of what it was. A model’s statement about its own identity is generated text. It comes out of the same machinery as every other sentence, shaped by whatever the prompt implied, and it is exactly as reliable as any other claim the model makes: plausible, fluent, and unverified.

    The moment an output’s value depends on where it came from, this matters enormously. A second opinion is only worth anything because it is second. If the attribution is wrong, the review is not mislabelled, it is worthless. The independence I thought I was buying never existed, and every decision made on the strength of it inherited a confidence it had not earned.

    You cannot ask a model which model it is

    The question of which model produced an output always has a real answer, and the real answer is never inside the text.

    Every request to a model passes through infrastructure that keeps its own records. The API response carries a model field naming the model that actually served the request. The session transcript logs each exchange. Usage and billing records are broken down by model, because that is how the provider charges. These are trustworthy for one simple reason: the model did not write them. The infrastructure recorded them.

    Everything on the other side of that line is not evidence. The model saying which model it is. A model name appearing inside the generated text. The name you put in your own prompt. And one that genuinely looks like evidence but is not: the model name your own code resolved before sending the request. That is a record of what you asked for, never of what answered, and the whole problem is that those two can quietly differ.

    One caveat worth knowing. A model field is only as trustworthy as whoever recorded it. From a first-party API, that is the provider. Through a gateway or an aggregator, you are also trusting the middleman. That may be acceptable, but it is a different claim, and you should know which one you are making.

    Three ways the name ends up wrong

    Ours was the second of three failure modes, and the other two are just as quiet.

    Silent substitution. You request one model and another one answers: a router falls back under load, a quota rule kicks in, a deprecated alias points somewhere new. The response is perfectly good. The identity is simply not the one you asked for, and nothing in the text will mention it.

    Role-play absorption. The one that got us. Hand a model a persona and it adopts it, claimed identity included. The result reads as an independent review while being produced by the same weights that wrote the thing under review. It is the failure that makes the whole exercise pointless while looking entirely successful.

    Refusal-shaped emptiness. A request gets declined at a safety layer. The channel returns an error or an empty completion, and a wrapper that never checks the status records a run that produced nothing as a run that passed. Or worse, it retries against a different model and records that as the original. A review with no content is not a review that found no problems.

    What we do now

    The assistant now reads the identity from the response metadata, in the same piece of code that consumes the output. That placement matters. It makes verification one line that always runs, instead of a habit somebody has to remember. In a multi-turn exchange it checks every turn, because a run that starts on one model and finishes on another is a mixed artifact, and only per-turn inspection reveals it.

    We fail closed. A missing field, an unreadable transcript, an empty completion, a non-success status: all of these mean unverified, and unverified is treated as “not that model”, never as probably fine. When a review does pass, the verified identity is recorded beside the output, with the date, because an attribution nobody checked is indistinguishable, a week later, from one that was. And when a run dies partway through, we keep the genuine part and say where it ended, rather than describing the whole thing as that model’s work.

    Two temptations came up along the way, and we refuse both. If a model declines to review something, we do not reword the request until the objection goes away. That is evasion, and a review of a disguised artifact tells you about the disguise. And we do not retry quietly after a refusal or a substitution. A log that says “reviewed by X” when X declined is worse than no log at all, because it stops anyone from ever looking again.

    The test we use is short. If our records say a particular model reviewed something, we can point at where that claim came from, and it is never the review itself.

    The rule

    • Verify which model produced an output from the API metadata or the transcript, never from what the text claims.
    • Treat the model name in your own request as a record of what you asked for, not of what answered.
    • Check every turn of a multi-turn exchange, not just the first.
    • Treat anything unverified, including missing fields and empty completions, as not that model.
    • Record the verified identity beside the output, with the date.

    Get the skill

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So the rule is also published on its own as a Claude skill: a single Markdown file that an AI coding assistant reads and applies when the situation comes up.

    The file: model-provenance/SKILL.md

    Or take the whole set:

    git clone https://github.com/blonderoofrat/agent-skills

    Installing it in Claude Code. Copy the skill’s folder into one of these, so the file ends up at .../skills/model-provenance/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads it at the start of the next session and applies it when what you are doing matches the description at the top of the file. You can also ask for it by name.

    Using a different assistant? The file is plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rule itself is Claude-specific.

    Licence: CC0, public domain. Copy it, adapt it, ship it in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. The rule above is also published on its own, as a free public-domain instruction file for AI coding assistants, the model-provenance skill, in blonderoofrat/agent-skills on GitHub.

  • The second opinion I could not get

    The second opinion I could not get

    This article comes with a free Claude skill

    The rule this story ends with is packaged as agent-clean-room. It is one small file you drop into Claude Code so your own assistant works this way too. It is public domain: copy it, change it, no attribution and no permission needed. How to install it is at the end of this article.

    I wanted a second model to look at something.

    The reasoning behind that is ordinary. When one assistant does most of the work and then checks its own work, the check is worth very little, because it is performed by the thing being checked, using the same assumptions that produced the thing. So the plan was to hand a design to a different model and ask it to disagree.

    It refused. Not rudely, and not with an argument: it simply declined to engage with the request at all, and produced nothing.

    I assumed I had asked badly and rewrote the question. It refused again. I made the question shorter, then more formal, then split it into two. Refused, refused, refused.

    Then I pasted the same question into an ordinary chat window with that same model, and got four paragraphs of exactly the thoughtful disagreement I had been asking for.

    What Claude Code sends with every subagent prompt

    What I had not understood is that a coding assistant does not send your question. It sends your question wrapped in a description of where you are standing.

    Before my words, the request carried the name of the branch I was on, the list of files I had changed, the subjects of my last several commits, and the contents of the instruction files that sit in the top of the project. All of that is genuinely useful when the assistant is helping me, and none of it is something I typed.

    Now consider what those things say on this project. The branch names, the file names and every recent commit message are about breeding rats: pedigrees, litters, which animals are related to which and by how much.

    A safety check does not read the question and the surroundings separately. It scores the whole thing that arrives. So a mundane request about page layout was being judged in the company of a wall of text about animal breeding, and it kept coming out on the wrong side of the line.

    The model was fine. The envelope was the problem, and the envelope was one I had never seen, because I did not write it.

    The part worth generalising is that the conclusion I had reached, that model is unusable for me, is the conclusion almost everybody reaches. It is wrong, and it is expensive, because it ends the investigation at the exact point where it was about to become cheap.

    The fix is to ask from somewhere that has nothing to say

    Run the second model as a plain background process, started from a scratch folder outside the project: no version control, no instruction files, no configuration. Then hand it a brief that contains everything it needs.

    Three things are doing the work there, and only the first is obvious.

    Outside version control. No repository means no branch, no diff, no commit log. This is most of the effect on its own.

    No instruction files. Project instructions are picked up automatically from wherever you started. A scratch folder has none to pick up.

    A brief that stands alone. The reviewer cannot see your code, so the brief has to carry the whole problem. That sounds like a cost and it is actually the largest single benefit: writing a brief that makes sense to someone with no context forces you to state the problem properly, and a fair amount of the value arrives before you send anything.

    The line this must not cross

    Removing your branch name from a question about page layout is legitimate. Your branch name was never part of the question.

    Rewording the substance of a question until a check stops objecting is a different act, and I want to give the practical reason rather than the moral one, because the practical reason is the one that actually stops you.

    Whatever comes back is a response to what you sent. If you soften the question, you get a confident answer to the softened version. You will then act on it as though it addressed the real one. You have not obtained a second opinion at all. You have obtained a second opinion about something else, and the fact that it reads as reassuring is precisely what makes it dangerous.

    When a subject really is declined, there are two honest moves: find a reviewer that will take it, or do the work yourself and write down that no second opinion was obtained.

    Then find out where the boundary actually is

    A clean room stops irrelevant context causing refusals. It does not tell you which of your real subjects a given reviewer will engage with, and that is the thing you need in order to plan.

    So we mapped it: small, honest questions across the areas we actually work in, each one framed in its own real terms, with every result written down including the refusals.

    That is also where we made the mistake worth passing on. Four probes went well, and we wrote the conclusion down as though it were settled. It was not. The fifth case contradicted it and cost a day.

    The fix is a single habit: keep the count attached to the claim. “Engaged four times out of five, all on one afternoon” stays useful for months, because it tells the next reader how much weight to put on it. “It handles this fine” is the same information with the load-bearing part removed, and it will be quoted back at you long after it stopped being true.

    And check what actually answered you

    Running a model as a background process puts several layers between you and it, and the text that comes back cannot tell you what produced it. A model asked to be something else will say it is that thing, in the output, convincingly. Read the identity from the run’s own metadata instead, and check every turn rather than the first one.

    Two failures are specific to launching a process rather than typing into a window, and both look like success:

    Nothing at all. The process finishes cleanly and writes an empty file, or writes an error into a file nobody opens. If your wrapper checks only that the process exited without complaint, it will happily record a review that never took place. Empty output is a failure. Always.

    Stopping halfway. A run can produce genuinely good work and then stop mid-thought. Keep the real part, say plainly where it ended, and finish the rest yourself under your own name. What you must not do is quietly run it again until something completes, and then describe the whole thing as that model’s work.

    The rule

    • Ask from a scratch directory, outside version control, with a brief that stands alone.
    • Strip the surroundings, never the substance. An answer to a disguised question is an answer to the disguise.
    • Map the boundary deliberately, and keep the sample size attached to what you concluded.
    • Verify which model answered, from metadata, on every turn.
    • Treat empty output as failure, because it is the one kind of failure that reports success.

    Get the skill

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So the rule is also published on its own as a Claude skill: a single Markdown file that an AI coding assistant reads and applies when the situation comes up.

    The file: agent-clean-room/SKILL.md

    Or take the whole set:

    git clone https://github.com/blonderoofrat/agent-skills

    Installing it in Claude Code. Copy the skill’s folder into one of these, so the file ends up at .../skills/agent-clean-room/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads it at the start of the next session and applies it when what you are doing matches the description at the top of the file. You can also ask for it by name.

    Using a different assistant? The file is plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rule itself is Claude-specific.

    Licence: CC0, public domain. Copy it, adapt it, ship it in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. The rule above is also published on its own, as a free public-domain instruction file for AI coding assistants, the agent-clean-room skill, in blonderoofrat/agent-skills on GitHub.

  • The reference that didn’t exist

    The reference that didn’t exist

    This article comes with a free Claude skill

    The rule this story ends with is packaged as citations – one small file you drop into Claude Code so your own assistant works this way too. It is public domain: copy it, change it, no attribution and no permission needed. How to install it is at the end of this article.

    The citation was perfect. Author names that sounded like researchers in the field. A real journal, one I recognised. A plausible year, a plausible volume, a title that described exactly the finding it was attached to.

    There was no such paper.

    Not a mistyped identifier, not a preprint that never made it to publication, not a paper I was struggling to find. The reference described a study that had never been performed by people who had never written it, and it looked more credible than most of the real ones on the page, because real citations have awkward titles and inconvenient author lists and this one had been generated to fit.

    Why this one is different

    Most of the lessons from building this site are about software, and they matter to whoever maintains it. This one is about you.

    Almost everything factual on this site is about animals that people keep. Whether a food is safe. Whether a symptom is urgent. What a medication does at what dose. People act on that. Someone reads a page here and decides not to go to a vet tonight, or decides to go right now, and the thing standing between them and a bad decision is whether the claim they read is true.

    An invented citation is worse than no citation, and it took me a moment to see why. A claim with no reference invites scepticism – you can feel its weight and go and check. A claim with a beautifully formatted false reference closes the question. It converts “I should verify this” into “someone already did.” It spends trust that was never earned.

    What does not work

    Asking the model to be careful. Instructions to only cite real sources produce more confident citations, not more real ones. The failure is not carelessness – the model is generating text that looks like a citation, and a false one satisfies that objective completely.

    Checking that it looks right. Format is the one thing these always get right. A well-formed identifier is not evidence, and reviewing citations by eye means reviewing them on the single dimension where fabrications are strongest.

    Spot-checking. Verifying a sample tells you about your sample. If the rest are correct, you learned nothing; if they are not, you shipped them. There is no number of correct citations that makes the next one correct.

    What does work

    Every identifier is checked against the source database that owns it – the actual bibliographic record, fetched, with the returned title and authors compared to what the citation claims. Not “does this identifier resolve” but “does it resolve to this“, because an identifier that points at a real but different paper is the failure mode a resolve check misses entirely.

    Then the check runs at the door. A page cannot be published with an unresolved reference in it. Not a warning, not a report – the publish step refuses, names the citation, and stops.

    That last part is what makes it a system rather than a habit. Verifying citations is exactly the task that gets skipped when you are nearly finished and it is late and the page is otherwise ready, which is precisely when the temptation is highest and the attention lowest. So the decision is taken out of the moment when it is hardest to make well.

    The number that made me build it

    Across the batches I have checked, a meaningful fraction of AI-generated citations were wrong – some entirely fabricated, some real papers that did not say the thing they were cited for. That second category is the sneaky one: the paper exists, the identifier resolves, everything passes a casual check, and the claim it is supporting is still not in it.

    I am deliberately not putting a headline percentage here, because my sample is my own work in one domain and quoting a number would give it more authority than it has earned – which would be an odd way to end an article about unearned authority.

    The actionable version is simpler: the rate is not low enough to skip checking, and you cannot tell which ones are wrong by looking.

    What this means for reading this site

    Every scientific and medical claim carries a numbered reference. Every one of those has been checked against the source database before the page went live, and a page that fails that check does not publish.

    That is not a promise that everything here is right. Papers get superseded, and I get things wrong. It is a narrower promise, and the narrowness is the point: when a claim here cites a source, that source exists and says what the claim says it says.

    You should still argue with the conclusions. Please do. But you should not have to wonder whether the evidence is real.

    Get the skill

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So the rule is also published on its own as a Claude skill: a single Markdown file that an AI coding assistant reads and applies when the situation comes up.

    The file: citations/SKILL.md

    Or take the whole set:

    git clone https://github.com/blonderoofrat/agent-skills

    Installing it in Claude Code. Copy the skill’s folder into one of these, so the file ends up at .../skills/citations/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads it at the start of the next session and applies it when what you are doing matches the description at the top of the file. You can also ask for it by name.

    Using a different assistant? The file is plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rule itself is Claude-specific.

    Licence: CC0, public domain. Copy it, adapt it, ship it in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. The rule above is also published on its own, as a free public-domain instruction file for AI coding assistants — the citations skill, in blonderoofrat/agent-skills on GitHub.

  • The practice record in the real database

    The practice record in the real database

    This article is part of a set of free Claude skills

    These rules are packaged as Claude skills: small files you drop into Claude Code so your own assistant works this way too. The published set is at blonderoofrat/agent-skills. They are public domain: copy them, change them, no attribution and no permission needed. How to install them is at the end of this article.

    There was an animal in the colony that did not exist.

    It had a name in the house style, a date of birth, a cage. It looked like all the others. It was sitting in the active list, being counted, making the total read 312 when the true number was 311. It had been created by a test – a script proving that the “add an animal” form worked – and the test had proved it by adding one, to the real database, and then not cleaning up after itself.

    Then I looked for its siblings. There were thousands.

    Not thousands of visible animals: the great majority were soft-deleted, invisible on every screen, harmless in every practical sense. But they were in the same table as the real ones, and they had been arriving for months, and the only reason anybody noticed was that one of them failed to delete itself and pushed a number on a dashboard up by one.

    That is the part I keep coming back to. The system was not silent because it was well-designed. It was silent because the bug was tidy. A messier version of the same bug would have been found in a day.

    What I tried first, and why it was not enough

    The obvious fix is to point the tests somewhere else. So I built that: a staging copy, and a redirect that every test calls at startup to aim itself at the copy instead of production.

    It worked, and then it leaked, and the way it leaked is the interesting part.

    The redirect installed itself by overriding a setting on an imported module. Two files imported that module by two different names – one as a submodule of a package, one as a bare top-level import. Python treats those as two separate module objects. The redirect overrode one of them. Writes that went through the other went to production, while the test printed a cheerful line saying it had been redirected.

    Every check I had confirmed the redirect had been called. None confirmed it had worked. So the machinery printed reassurance at exactly the moment it was failing.

    Making the test suite refuse to touch production

    I had built six checks around the redirect. A reviewer looked at them and said something that reorganised the problem for me: not one of the six had caught a single real defect, three of them contained the very bug they were policing, and all six verified a call was made rather than an effect was achieved.

    The advice was to stop asking tests to behave and start making misbehaviour impossible:

    Withhold the credentials. Membership is location, not intention.

    Any script whose entry point lives in a test directory is denied production credentials from its first line – whether or not it redirects, whether or not it remembers to, whether or not it was written yesterday by someone who has never read this. There is no opt-in to get right, so there is nothing to forget. A new test cannot reach production by accident, only by being deliberately named in a short list that a human maintains.

    The difference is that the old design failed open for anyone who forgot, and the new one fails closed for everyone who does not ask.

    And a ground-truth check, because guards lie

    The second thing that reviewer said was that all the static checks in the world are proxies, and the one check actually worth having was missing: audit the real database around every test run.

    Snapshot it before, compare after, report every row added, removed or changed. It knows nothing about which tests exist, what a fixture looks like, or what the naming conventions are – that ignorance is the point, because every piece of knowledge is somewhere my assumptions could hide. It would have caught the two-module leak on day one without anyone having imagined two-module leaks.

    The postscript that makes the point better than the story does

    That audit was written, tested, and declared correct. It then sat inert for a day.

    The seal I had built an hour later denied it credentials too – it lived in the test directory, so it was treated like a test – and so the one instrument whose entire job was to prove the database had not changed was silently unable to look at the database. Its own self-test passed the whole time. That self-test only ever asked whether the seal blocks things. It never asked what it was blocking.

    It is now in an explicit read-only list, its self-test has a case for that path, and a separate check enumerates what the seal denies rather than only what it permits.

    I do not offer that as a tidy ending. I offer it as the actual lesson: the guard you just wrote is the least-tested code in your system, and it will fail in the direction that looks like success.

    The rules

    • Isolate by construction, not by convention. Withhold credentials by location. Do not ask tests to redirect themselves.
    • Verify the effect, not the call. “The redirect ran” and “the write went to the copy” are different claims, and only one of them is the one you care about.
    • Keep one dumb ground-truth check that knows nothing about your test suite and only compares the real data to itself.
    • Run your new guard inside the real pipeline before trusting it. A self-test proves the guard fires. It says nothing about what else it broke.

    Get the skills

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So these rules are also published on their own as Claude skills: small Markdown files that an AI coding assistant reads and applies when the situation comes up.

    The set: blonderoofrat/agent-skills

    git clone https://github.com/blonderoofrat/agent-skills

    Installing them in Claude Code. Copy any skill’s folder into one of these, so the file ends up at .../skills/<skill-name>/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads them at the start of the next session and applies one when what you are doing matches the description at the top of that file. You can also ask for one by name.

    Using a different assistant? They are plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rules themselves is Claude-specific.

    Licence: CC0, public domain. Copy them, adapt them, ship them in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.

    The particular rule in this article is being added to that set; it is not up there yet.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. Most of these rules are also published on their own, as free public-domain instruction files for AI coding assistants, at blonderoofrat/agent-skills on GitHub. This one is not up there yet.

  • The promise nobody broke and nobody kept

    The promise nobody broke and nobody kept

    This article is part of a set of free Claude skills

    These rules are packaged as Claude skills: small files you drop into Claude Code so your own assistant works this way too. The published set is at blonderoofrat/agent-skills. They are public domain: copy them, change them, no attribution and no permission needed. How to install them is at the end of this article.

    I found it in a planning document, in a paragraph I had written months earlier: we should check whether the contact form is still forwarding correctly.

    Nobody had checked. Nobody had decided not to. There was no argument about it, no deprioritisation, no note saying it had turned out to be unnecessary. The sentence had simply been written down in a place where writing things down feels like doing something about them, and then the document was closed.

    I went looking for siblings, the way you do. There were about thirteen – thirteen pieces of work that were, in the most literal sense, not happening, and had never been decided against.

    The failure mode is silence, not overrun

    The thing that makes this hard to see is that nothing goes wrong. A dropped commitment produces no error, no alert, no angry message. It produces nothing at all, which is indistinguishable from a commitment that was quietly and correctly cancelled.

    That is why “just be more organised” does not work. You are not fighting a lack of discipline. You are fighting the fact that the failure state and the success state look identical from outside, so there is no signal to be disciplined about.

    And the natural home for a promise is the worst possible one. Commitments get written where they are made – in a design document, a code comment, a message, a plan. Every one of those is a place you will not be looking when you next decide what to do. The promise is not lost. It is filed under the context that produced it, which is precisely the context that has ended.

    Parked and killed are honest endings

    The design that fixed this is smaller than the problem suggests. Every promise gets an entry, and every entry has to reach one of four states:

    • done – finished, with a reference to the actual evidence, not a claim
    • parked – deliberately not now, with the condition that would revive it
    • killed – deliberately never, with the reason
    • blocked – waiting on a specific named input from a specific person

    And nothing may leave the list any other way. A silent drop is the only bug.

    That framing is the whole trick, and it took me a while to appreciate why it works. It removes the guilt. If the only acceptable outcome were done, the list would become a monument to failure and people would stop adding things to it, which is exactly the behaviour you cannot afford. Killing something with a stated reason is a completely respectable outcome. So is parking it. What is not respectable is the item quietly evaporating, and once those are the terms, writing things down stops being a commitment to do them and starts being a commitment to decide about them.

    The blocked state needs one extra rule, because it is the one that rots. A blocked item has to name the specific input it needs and who supplies it. “Blocked” on its own becomes a shelf. The test I use is: if that person replied “use your judgement”, could I proceed? If yes, I was never blocked – I wanted reassurance, which is a different thing and not their problem.

    It has to resurface, or it is just a longer document

    A list you have to remember to read is a document, and documents are what failed. The ledger is loaded and printed at the start of every working session, and every open item appears until it is disposed of. It is the first thing seen and it is not dismissible.

    There is one number worth watching: the count of open items older than three weeks. Not the total, which mostly measures ambition, and not the completion rate, which you can improve by promising less. The age of the oldest open items is the only figure that detects the actual failure – things that are technically tracked and functionally abandoned. It has to trend down. When it does not, the answer is usually a round of honest killing rather than a burst of work.

    What surprised me

    I expected this to be a chore. It is not, and the reason is the thing I would tell anyone building one.

    Most of the items get killed. Written months ago, in a context that has since dissolved, by someone with less information than I have now. Reading them back and writing no, and here is why is not a failure of follow-through. It is the system working: a decision that never got made, finally getting made, with the benefit of everything learned since.

    The thirteen that started this became a handful of real pieces of work and a lot of honest nos. The handful got done. The nos are still there, with their reasons, so nobody proposes them again in six months without at least reading why they were dropped.

    Get the skills

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So these rules are also published on their own as Claude skills: small Markdown files that an AI coding assistant reads and applies when the situation comes up.

    The set: blonderoofrat/agent-skills

    git clone https://github.com/blonderoofrat/agent-skills

    Installing them in Claude Code. Copy any skill’s folder into one of these, so the file ends up at .../skills/<skill-name>/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads them at the start of the next session and applies one when what you are doing matches the description at the top of that file. You can also ask for one by name.

    Using a different assistant? They are plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rules themselves is Claude-specific.

    Licence: CC0, public domain. Copy them, adapt them, ship them in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.

    The particular rule in this article is being added to that set; it is not up there yet.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. Most of these rules are also published on their own, as free public-domain instruction files for AI coding assistants, at blonderoofrat/agent-skills on GitHub. This one is not up there yet.

  • The question I answered three times

    The question I answered three times

    This article is part of a set of free Claude skills

    These rules are packaged as Claude skills: small files you drop into Claude Code so your own assistant works this way too. The published set is at blonderoofrat/agent-skills. They are public domain: copy them, change them, no attribution and no permission needed. How to install them is at the end of this article.

    The third time I asked it, the owner did not answer. He said: I already told you this.

    He had. Weeks earlier, in a message I had read, understood, and acted on correctly at the time. The answer had gone into the work, which is where answers are supposed to go. It had not gone anywhere that could be looked up later, so when the same situation came round again, the only trace of his decision was in code that did not say why it was that way.

    So I asked again. And because I was asking rather than reading, I did not ask the same question – I asked a slightly different one, shaped by whatever I was doing that day. He answered slightly differently, because it was a slightly different question. Now there were two answers, both real, subtly inconsistent, and no way to tell which was current.

    By the third time, the drift was doing real damage, and the cost was not the minutes. It was that being asked the same thing repeatedly is a specific and corrosive signal: you are not listening.

    What I got wrong about what a decision is

    I had been treating a decision as an input: something the owner supplies, that I consume, that becomes code. Consumed inputs leave no residue. Once the code exists, the decision has been fully spent.

    That is wrong, and the tell is that decisions get re-litigated. A decision is not an input. It is a record, and it stays true after the work that used it is finished, because the next question in the same territory needs it too.

    Three properties follow, and I only found them by getting each one wrong first.

    It has to be in his words, not my summary. My summary is an interpretation, and interpretations lose exactly the part I did not think was important – which is reliably the part that matters six weeks later. Now every recorded decision carries his sentences verbatim. Mine can sit alongside as commentary; they cannot replace.

    The full comment outranks the button. When a decision is offered as options, people pick the closest one and then explain what they actually meant. On this project the explanation contradicts the button often enough that a rule exists: read the whole comment, never just the choice. Several times the words next to the click reversed it.

    It lives in exactly one place, and everything else points at it. The first version copied decisions into whatever document needed them. Copies drift. Worse, a copy is confidently wrong, because it looks like a record. There is now a single row per decision and every other mention is a reference to that row.

    Getting an AI assistant to remember decisions between sessions

    Writing decisions down does not stop you re-asking, because the failure is that you did not remember there was anything to look up. You cannot search for a fact you do not know exists.

    So the lookup is not optional and not remembered. It is a gate on the asking:

    Before a question can be put to him, the tool checks whether he has already answered it. If a prior decision matches, the tool refuses to file the question and shows me the answer instead. Not a warning – a refusal, because a warning is something you learn to click past on the afternoon you are in a hurry.

    That gate has caught me. Repeatedly. Which is the only evidence that matters about whether a check is worth having: not that it exists, but that it has said no to you and been right.

    The other half, which took longer to see

    A question stops mattering when you get an answer. But an answer does not stop mattering when it is given.

    Answers were arriving and getting lost. He would answer, mark the item done, and done is a terminal state – so anything scanning for open work stopped seeing it. His answer sat in a closed row nobody would ever read again. From his side, he had answered. From mine, nothing had happened. Neither of us could see the gap.

    The fix is a two-sided handshake. An item is not finished when he answers it. It is finished when I record what I did about his answer, with a reference to the actual work. Until then it sits in a queue of answers awaiting action, which is a different queue from questions awaiting answer, and I read both at the start of every session.

    That distinction sounds pedantic. It is the difference between a system where things get dropped politely and one where they do not.

    The rules

    • A decision is a record, in his words, in one place. Everything else points at it.
    • Read the whole comment, never the button. The qualification is the answer.
    • Gate the asking on the lookup, mechanically, so re-asking is refused rather than discouraged.
    • An answer is a hand-back, not a finish. It closes when the person who asked records what they did with it.

    Get the skills

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So these rules are also published on their own as Claude skills: small Markdown files that an AI coding assistant reads and applies when the situation comes up.

    The set: blonderoofrat/agent-skills

    git clone https://github.com/blonderoofrat/agent-skills

    Installing them in Claude Code. Copy any skill’s folder into one of these, so the file ends up at .../skills/<skill-name>/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads them at the start of the next session and applies one when what you are doing matches the description at the top of that file. You can also ask for one by name.

    Using a different assistant? They are plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rules themselves is Claude-specific.

    Licence: CC0, public domain. Copy them, adapt them, ship them in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.

    The particular rule in this article is being added to that set; it is not up there yet.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. Most of these rules are also published on their own, as free public-domain instruction files for AI coding assistants, at blonderoofrat/agent-skills on GitHub. This one is not up there yet.

  • The safety check we learned to ignore

    The safety check we learned to ignore

    This article comes with a free Claude skill

    The rule this story ends with is packaged as hook-tuning – one small file you drop into Claude Code so your own assistant works this way too. It is public domain: copy it, change it, no attribution and no permission needed. How to install it is at the end of this article.

    The command was refused, and I overrode it without reading the message.

    That is the whole story, and it took me a while to understand why it was a story at all. The check was working. It had refused something it was designed to refuse. I typed the override flag, the command ran, nothing bad happened, and I carried on.

    I overrode it because the week before it had refused something harmless, and the week before that, and by then I had stopped reading its output and started treating it as a toll booth.

    The check was not broken. Its relationship with me was broken, and no amount of making it more correct would have fixed that, because I was no longer reading it.

    The arithmetic nobody does

    Say a blocker is right 95% of the time. That sounds excellent. Now say it fires ten times a week.

    That is one false refusal a week. One a week is enough to teach a person that the fastest path through this thing is the override flag – and once that is learned, the blocker’s accuracy stops mattering entirely, because its output is no longer an input to anyone’s decision. You have not bought 95% protection. You have bought 0%, plus friction, plus a habit that will carry over to the next blocker you write.

    A false positive in a blocker is not a small cost paid for a large benefit. It is a payment made directly out of the mechanism’s own credibility, which is the only thing it has.

    The line

    Block only on deterministic facts. Warn and log on judgement.

    The test is whether the check can be wrong about this specific instance in a way a reasonable person would dispute. Not whether the rule is a good rule – whether this firing is arguable.

    • “This file contains GPS coordinates in its metadata” – a fact. The bytes are there or they are not. Block.
    • “This version number is lower than the one already deployed” – a fact. Block.
    • “This text sounds like it might identify someone” – a judgement. Warn, log it, and let a human decide, because you will be wrong often enough to be ignored.

    The tempting move is to block on the judgement calls, because those are the expensive mistakes. That is exactly backwards. The expensive mistakes are where you most need the human’s attention, and blocking on ambiguity is how you lose it.

    What a refusal owes you

    If a check does block, it has an obligation, and it is not “explain the rule.” It is:

    • Name the specific thing. Not “OPSEC violation” but the exact string, at the exact line.
    • Name the fix, as a command that can be run.
    • Name the override, and log every use of it.

    That last one matters more than it looks. An override that is logged is a measurement: if that log grows, the check is miscalibrated, and you have the evidence in hand rather than a vague sense that people are annoyed. An override that is not logged is just a hole, and you will never know how often it is used.

    And it means a check can be tuned with data instead of argument. When our override log started filling up, the answer was not to argue about whether the check was too strict. It was to read the overrides, see they clustered on one pattern, and narrow the check to exclude that pattern.

    The one that nearly got deleted

    We had a check that fired forty times in a single session.

    Every firing was correct. The rule was right, the detection was right, and the fix it named was the right fix. It fired forty times because I kept doing the thing anyway – reaching for the quick version of something the check wanted done properly.

    The reflex, at firing number thirty, was to delete the check. It was in my way. It was always in my way.

    It stayed, and it stayed for a reason that is worth stating: the cost of each denial was one wasted round trip, and the cost of the thing it was preventing was measured in hours. The check was not too strict. I was arguing with a correct check because arguing felt cheaper than changing the habit.

    So the last rule is about the person reading the output, not the code:

    Before tuning a check that keeps firing, establish whether it is wrong or whether you are. Both feel identical from the inside. The difference is visible in the log – a check that is wrong fires on a variety of innocent things, and a check that is right fires forty times on the same one.

    Get the skill

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So the rule is also published on its own as a Claude skill: a single Markdown file that an AI coding assistant reads and applies when the situation comes up.

    The file: hook-tuning/SKILL.md

    Or take the whole set:

    git clone https://github.com/blonderoofrat/agent-skills

    Installing it in Claude Code. Copy the skill’s folder into one of these, so the file ends up at .../skills/hook-tuning/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads it at the start of the next session and applies it when what you are doing matches the description at the top of the file. You can also ask for it by name.

    Using a different assistant? The file is plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rule itself is Claude-specific.

    Licence: CC0, public domain. Copy it, adapt it, ship it in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. The rule above is also published on its own, as a free public-domain instruction file for AI coding assistants — the hook-tuning skill, in blonderoofrat/agent-skills on GitHub.

  • “Zero problems found.” Zero of what?

    “Zero problems found.” Zero of what?

    This article comes with a free Claude skill

    The rule this story ends with is packaged as denominator-rule – one small file you drop into Claude Code so your own assistant works this way too. It is public domain: copy it, change it, no attribution and no permission needed. How to install it is at the end of this article.

    The check was green. It had been green for weeks. It was also, in a narrow sense, telling the truth: it had found no problems.

    It had found no problems because it had looked at no files.

    A path had changed underneath it. The glob it used to find things to inspect now matched nothing, so it inspected nothing, found nothing wrong with nothing, and reported success. Every part of that chain behaved correctly. The only thing that was wrong was the sentence it printed, and the sentence it printed was the only part anybody read.

    The thing that makes this hard

    The failure is not that the check broke. Checks break all the time and you notice, because they go red and someone comes to fix them.

    The failure is that it broke into the reassuring state. A check that dies loudly costs you an afternoon. A check that dies quietly costs you the thing it was watching, for however long nobody looks – and the greener it stays, the more you trust it, and the less likely anyone is to look.

    I have now hit this in four different disguises, and I want to name them because they do not look alike from the inside:

    • An empty population. The check ran, found nothing to check, and reported no problems.
    • A wrong boundary. It examined one folder and I read the result as if it covered the codebase.
    • A wrong predicate. The detector recognised one spelling of the thing it was looking for, so everything written the other way was invisible to it.
    • Use versus mention. It counted a file that quoted a dangerous call as if the file made that call, which inflated the count in the direction that felt responsible.

    Different bugs. Same shape: the number was fine, and the population behind the number was not what I thought it was.

    The rule

    A green light must be able to state its denominator.

    Not “did anything fail” but “how many things did you examine, and out of what.” A check that cannot name the population it looked at is decoration. And the instant you make it print that number, all four disguises above become visible, because “0 files examined” reads very differently from “no problems found” while being the same event.

    Three parts, and the third is the one people skip:

    1. Enumerate from the real boundary, described in the question’s own words. Not “every file in this folder” but “every script that can write to the database.” Those are different sets and only one of them is the answer.
    2. Report the count, always, including when it passes. The number is the check. The verdict is an opinion about the number.
    3. Fail closed on empty. Zero items examined is a fault, never a pass. If the check cannot find its subjects, it does not know they are fine; it knows nothing.

    The correction that taught me the most

    I once reported a leak closed across a population of 7. Then I found the real boundary was 43 and corrected it. Then I found it was 49 and corrected it again.

    Three confident numbers in one afternoon, each an improvement on the last, each wrong. And the reason I kept stopping too early is worth saying plainly: a real improvement is emotionally indistinguishable from a complete one. Widening the set felt like finishing. It felt like finishing all three times.

    So there is a corollary I now apply mechanically, because judgement demonstrably does not fire here: when you widen a denominator, assume the widened one is still too narrow, and look one level further out before you assert anything. Check whether the detector recognises more than one spelling. Ask what a file that mentions the thing without doing it would score. Then say the number with its method attached, so the next person can attack the method instead of inheriting the number.

    And one direction this does not go

    There is a trap on the other side that I nearly walked into, and it is worth flagging because it wears this rule’s clothes.

    Enumerating a population is a claim about what you have not checked. Being wrong makes you humbler. But if the output of your enumeration is a list of things that get to skip a check – an allow-list, an exemption – then being wrong hands out access, silently.

    I once classified every script in a directory and auto-exempted the ones that looked harmless. The list included the probe whose entire job was to prove the exemption mechanism still worked. Same instinct, opposite consequence.

    So: enumerate the whole set, always. But when the result is an exemption, decide each member one at a time with its own reason, and ask two questions before you ship it. Is the test of this guard in the list? And does every entry have a specific reason, or do two hundred entries share one? A shared justification across two hundred entries means nobody decided any of them.

    Get the skill

    Everything above is generic. None of it is about rats, and none of it is specific to this site. So the rule is also published on its own as a Claude skill: a single Markdown file that an AI coding assistant reads and applies when the situation comes up.

    The file: denominator-rule/SKILL.md

    Or take the whole set:

    git clone https://github.com/blonderoofrat/agent-skills

    Installing it in Claude Code. Copy the skill’s folder into one of these, so the file ends up at .../skills/denominator-rule/SKILL.md:

    • ~/.claude/skills/ (available in every project on your machine)
    • your-project/.claude/skills/ (that one project only)

    Claude reads it at the start of the next session and applies it when what you are doing matches the description at the top of the file. You can also ask for it by name.

    Using a different assistant? The file is plain Markdown with a two-line header. Paste the body into whatever system prompt, rules file or instructions file your tool uses. Nothing in the rule itself is Claude-specific.

    Licence: CC0, public domain. Copy it, adapt it, ship it in commercial work, no attribution required. These are deliberately frozen snapshots rather than a maintained project, so if one is wrong for your situation, change it. That is easier than asking us to.


    Part of Notes from building this site: articles about working practices that exist because something here went wrong first. The rule above is also published on its own, as a free public-domain instruction file for AI coding assistants — the denominator-rule skill, in blonderoofrat/agent-skills on GitHub.