27. September 2026
Every time you ask someone to tell your tool something it could have found out, you’ve created a second copy of a fact. Second copies drift. In an agent’s toolchain there are a lot of facts, and until recently I was asking for a lot of second copies.
This is the first post about myspec. It’s a Rust CLI that wraps Claude Code and keeps my repositories consistent and their agents well-equipped. It started as a personal spec-driven development workflow, meaning the agent works from a written change proposal and its spec deltas rather than straight from a prompt. Then on 18 September it changed direction. The design doc records the reason as a critique of the original plan: “The spec workflow alone doesn’t keep repositories consistent as practice moves, doesn’t make sure agents have their tools, and can’t tell a running session what happened in the repository.”
The work so far is 30 commits, dated 13 to 24 September. Most of the interesting ones landed in the second week, and they share one idea, which I want to argue for here. Once an agent’s toolchain has enough moving parts, it needs the discipline of a real system. That means schema migrations, gates that fail loudly, and one source of truth per fact, not convention and a hope that everyone remembered. The clearest example turned out to be the smallest one: a flag that said which mode I was in.
myspec’s change workflow has two modes, specify and build. In specify, the agent may edit specs, the change folder, architecture decisions and module rules, but not production code. In build, it implements against an approved change. You switch with a myspec command. The switch rewrites the repository’s local Claude Code hook settings, swaps a skill folder, and tells myspec’s own MCP server to change its tool list. A PreToolUse hook then refuses edits the mode doesn’t allow.
Mechanically, it works. A spike on 13 September confirmed that Claude Code 2.1.270 re-requested myspec’s tool list within a second of the switch, with no restart. The problem wasn’t mechanical. The architecture decision record (ADR) I wrote about modes puts it plainly:
it asks the developer to declare their intent a second time, in a second place, at a terminal, while they are working. So it goes stale in one of two directions: left in
specifyit blocks legitimate code edits; left inbuildit leaves spec work ungated. The cheap way out ismyspec mode off, and a guard rail people switch off protects nothing.
The phrase that matters there is “a second time”. I already tell Claude Code what kind of work I’m doing. Shift+Tab into plan mode says “I’m thinking, not building”, and leaving plan mode says “now build”. That gesture is free, it’s already part of how I work, and it’s the only declaration made at the moment the work actually changes. The mode command asked me to say the same thing again, somewhere else, and to remember to say it every time.
There’s a detail in how myspec is set up for its own repository that I find telling. Mode switching is turned off there, because switching would change the hooks and MCP servers of the very session developing myspec. The tool that invented the mode flag couldn’t use it on itself. That’s a practical constraint rather than a verdict on the flag. But it does mean the flag was never dogfooded where I spend most of my time.
The modes ADR, “Follow the developer’s declared intent, rather than being told twice”, was proposed on 19 September and revised the same day. Its recommendation is to read Claude Code’s permission_mode instead of asking for a declaration. That field rides on the hook payloads myspec already receives, and its values are default, plan, acceptEdits, auto, dontAsk and bypassPermissions. Plan mode maps to specify, default and acceptEdits map to build. myspec’s hooks have been receiving that field all along and ignoring it; they read only the tool name, the tool input and the working directory.
No hook fires when the mode changes, so detection happens on the next prompt or tool call after the switch. The ADR treats that as a feature: myspec “never acts on a switch the developer has not yet acted on themselves.” The rest of the loop is already built. The hook writes a state file, the MCP server’s watcher notices on its 500 ms poll and sends tools/list_changed, and the session re-requests its tools. The ADR estimates that at “under about a second and a half, end to end.”
I want to be exact about the status, because it would be easy to overclaim. The ADR is Proposed, not accepted. The code doesn’t read permission_mode yet; it appears only in the ADR and a test checklist. The whole proposal depends on a second spike, a manual checklist whose boxes are all unticked. It asks whether permission_mode really arrives on myspec’s hook payloads. It asks whether ExitPlanMode reaches a PreToolUse hook and can be denied, which is undocumented. And it asks whether a skill swap written from inside a hook gets picked up. So this is a decision about where the truth lives, not yet a feature. I think the reasoning is worth writing down before the spike, because the reasoning is the part that generalises.
The move in the ADR that I’d defend hardest isn’t “read the mode”. It’s the observation that the flag was doing two jobs:
Enforcement — may this edit happen? — is inherently per edit. […] Posture — which skill is loaded, which tools exist, what context each prompt carries — is inherently per session, and is exactly what should follow the developer’s direction.
When both jobs share one flag, a wrong guess about posture blocks an edit. Split them, and a wrong guess costs you a misplaced skill. That’s what makes it safe to follow a signal that’s a good indication rather than a proof. And plan mode is only a good indication. You can plan a refactoring, which isn’t spec work. The ADR admits this and accepts it only because of the split: “the whole argument rests on a wrong guess being cheap.” It adds that if posture ever gains enforcing power, the ADR is void.
Enforcement moves somewhere else in the proposal. The PreToolUse verdict would judge the edit path against the state of the change itself, not against a session mode. For example, it would check whether this change’s specs have been approved. Approval is a human act that myspec already records, with a fingerprint that goes stale when the approved files are edited. So enforcement also stops depending on a second declaration. It reads a fact that already exists, in the one place it’s recorded.
The ADR went through the alternatives, and two of them are worth mentioning because they’re the obvious ones. Configuring everything at launch, with the mode fixed for the session, was the original design. It fails because the things chosen at launch are exactly the things Claude Code can’t change mid-session. A plugin set needs /reload-plugins typed in the session, and which MCP servers run is fixed at session start. So every switch would mean a restart, and “a session is where the context, the plan and the thread of work live.” Inferring the mode from which paths are being edited was rejected as the primary signal because “it guesses at intent that C can simply read.” It survives as a supplement.
There’s also a small measurement in the ADR that I liked, because it answered a question I’d been treating as obvious. I’d assumed per-mode tool lists were partly about saving context. They’re not. myspec’s whole tool list is 961 bytes, about 240 tokens, and swapping it per mode saves about 60. So the per-mode list exists to steer the model, by making the wrong tool absent. It doesn’t save anything worth counting, and keeping the server running costs nothing to speak of.
Once I’d seen the mode flag as a second copy of a fact, I started seeing the same pattern in the rest of the change of direction. The three capabilities built on 18 and 19 September each address a place where two copies of something could quietly disagree.
Toolchain provisioning. What an agent can do in a repository depends on which plugin marketplaces, plugins, skills, MCP servers and CLI tools happen to be installed on the machine. The toolchain ADR names the problem: “Two clones of the same repository could give an agent different capabilities, and neither would report the difference.” The fix is a toolchain section in myspec’s per-repository config as the one declaration. A sync command writes that declaration into the committed Claude Code settings, MCP server list and skills folder, and a check confirms the committed files still match it. The ADR describes the result as “one home and three readers.”
That declaration taught me something straight away. The design originally said that once a collaborator trusts the folder, Claude Code offers to install the declared plugins. On 20 September I had to correct that. Claude Code’s own docs now say that since v2.1.195, a plugin enabled only by the project’s settings doesn’t load until someone installs it. It showed up in another of my repositories, where the committed settings enabled all seven private plugins and the check passed, while Claude Code’s plugin list showed none of them. So the committed files are a statement of what should be there, not proof that it is. A status command now checks each machine and prints the install command for anything missing. The design doc is candid about what’s left: the declaration buys agreement on “what should be there”, and turns a missing plugin into “a named failure rather than a silent difference between two machines.”
Profile migrations. myspec used to lay a repository’s conventions down once, at creation. That meant layout, AGENTS.md, prek hooks (prek is the pre-commit runner I use), module rules and CI. When the profile improved, older repositories never heard about it. The migrations ADR says it in one line: “Scaffolding that only runs at creation time guarantees drift.” The other obvious fix is to let myspec rewrite whatever files it finds, and the ADR dismisses that as “how configuration tools clobber work people did by hand.”
The answer is borrowed from databases. A profile is now an ordered list of migrations, embedded in the binary. Each repository records the profile version it’s at. A migrate command applies whatever comes after that version, in order, and stops at the first migration that doesn’t fully pass, leaving the version where it was. An adopt command brings an existing repository in at version 0 and migrates it. It creates only missing convention files, never touches the project manifest or the source code, and never overwrites anything.
The rules around it are the ones you’d expect from anyone who has run schema migrations in production. Every edit to an existing file goes through colab, my rule-driven edit tool, which had to gain TOML, JSON and Markdown backends first. Recording the new version is itself a colab step, “so the rule has no exception”. There’s no manual step kind, because “a step with no check could never complete.” Migrations are append-only. When I needed to add private plugins, it became a sixth migration rather than an edit to the fifth, because the fifth was already published and applied, and a repository already at version 5 would never see a change made inside it. Per-clone setup like installing the git hooks writes into the local git directory, which a committed version number can’t describe. So it runs on every migrate whatever the version, and skips itself when its effect is already there.
A migration is small. The second one, for example, adds a pre-commit hook that validates specs whenever specs or change proposals change, and it’s a single colab rule: find the hook entry in the prek config, and insert it if it isn’t there. Inserting rather than setting matters. It means a repository that already has its own entry keeps it. The Rust profile is at version 6 now, and the myspec repository is at version 6 itself.
Supervised sessions. The third capability launches Claude Code through myspec and pushes repository events into the running session through Claude Code Channels, which are in research preview. Those events include commits, branch switches and failed gates. Before this, the only way for the agent to learn that a hook had failed in another terminal was for me to tell it, which is yet another hand-relayed copy of a fact the repository already had. It’s verified to work interactively. The design doc records a gate-failed event reaching a session a second after it was queued. It doesn’t work under claude -p, where the development flag’s confirmation prompt can’t be answered.
The other half of treating a toolchain as a system is refusing to let a check pass when it didn’t actually check anything. myspec’s gate engine went in on 13 September. It’s one registry of checks that serves myspec’s report, the mode gates and the MCP gate tools. Every check returns pass, fail, or unavailable with a reason. “Unavailable” exists so that a missing tool is reported as missing rather than counted as a pass.
The change I think best shows this discipline is a small one. tropism is my architecture checker. I wrote about it in August: a ruleset of module dependency rules, checked across ten languages with no build and no network. One of myspec’s checks asks whether that ruleset exists, has rules, and gives each one a reason. Another asks whether the code violates them.
The two tools had a contract, and tropism changed its side of it. tropism 2026.9.0 reported a ruleset it couldn’t load as a failed rule check inside its JSON, with exit code 0. 2026.9.1 exits 2, prints nothing on stdout, and writes the error on stderr. Against the new contract, myspec’s JSON parser would have found no JSON and reported that tropism produced no usable result. That’s an unavailable, not a failure. A ruleset using a rule kind tropism doesn’t implement would then pass the ruleset check with a note saying tropism wasn’t consulted.
The fix reads both contracts and tells a ruleset error apart from every other exit 2. The test that came with it asserts the property I care about: a ruleset tropism refuses must not pass. There’s a matching test that a missing scan root, which is also exit 2, is not treated as a ruleset error. Both tests run against output captured from both tropism versions.
I think this is what a toolchain looks like once it’s a system. The dependency between myspec and tropism is a contract between two versioned tools I wrote myself, and even so it broke silently in the safe-looking direction. The gate has to be written so that a contract change it doesn’t recognise can’t turn into a pass.
The same thinking drove a later ADR, on waivers. The myspec repository can never pass the toolchain check, because running the sync there would rewrite the configuration of the very Claude Code session developing myspec. A permanent red “teaches people to ignore the report”. Loosening the check weakens it everywhere. So a waiver entry carries the standard, a reason and an optional expiry. The report then shows the failure as waived, with its evidence and reason, rather than turning it into a pass. The dogfooding work made the myspec repository pass its own report, and the waiver is how it keeps passing without pretending.
This is one tool, about two weeks of commits, and an ADR that’s still a proposal waiting on a spike. That’s a worked example, not evidence of a general law. What it suggests is this.
Each piece of operational discipline in myspec answers one specific drift bug I either hit or could see coming. The mode flag went stale. Repositories created before a convention never got it. Two clones gave an agent different tools. A tropism upgrade would have turned a broken ruleset into a pass. None of these fixes is novel. Ordered, append-only migrations with a recorded version, gates with a third state for “didn’t run”, and waivers with reasons are all things we already do for databases and CI. What changed is that an agent’s working environment now has enough parts (plugins, marketplaces, skills, MCP servers, hooks, modes, the tools behind each gate) for the same failures to show up there too.
The mode flag is the lesson I’d generalise most carefully, because its fix is the least mechanical. A migration or a gate adds structure. The modes ADR removes a declaration. It stops asking me for something Claude Code already knows, and reads it from there. The honest cost is that I’m then following someone else’s signal. A new permission mode, or a change in what plan mode means, moves myspec’s posture along with it. So the ADR asks for the mapping to live in one table with a recorded Claude Code version, and for every automatic flip to be logged and announced, because “a change the developer cannot see is worse than a stale flag.”
If the spike shows that ExitPlanMode can be denied from a hook, leaving plan mode with an unapproved change becomes the gate: the check happens at the moment I’ve just acted, rather than through a command I have to remember. If it can’t, the same message goes through the channel as advice instead. Either way, I no longer have to declare the mode twice.