I have 51 command-line tools on this machine. I did not plan for that number. It happened because a CLI is the shortest distance between an idea and something I can actually run, and because for the last several years I have had help writing them faster than I could have alone.
That help came with a habit I noticed early, back when GPT-3.5 and the first Claude models were the ones I worked with. Ask 3 different models to scaffold a CLI and you get 3 different opinions about what a CLI is. One reaches for Commander. One reaches for Inquirer for the prompting. One reaches for Chalk because output should be colored. Each answer is defensible. Together they are a tax, because now I own 3 codebases that disagree about argument parsing, about what a failure looks like, and about which of those libraries I am now responsible for watching.

Every model reached for a different set of libraries, and I was the one who had to live with all of them

The tax is not the libraries. It is that improvements stop travelling.
When 3 CLIs disagree about how a command reports failure, a fix in one is a fix in one. There is nothing to upstream it into. The work does not compound, and after the tenth tool you are not building leverage, you are maintaining a portfolio of near-misses.
I had already written about wanting the opposite of that. The whole argument in How to Turn AI Gains Into Compounding Infrastructure is that a gain becomes durable when every dependent project inherits it. A shared capability surface. A promotion rule. One place where an improvement lands and spreads.
I had built that layer for AI capability, for workflow, for operations. I had not built it for the thing I actually make most often.

So I built one base and made every CLI project upstream its improvements into it

The rule was simple and it was mine to enforce: when a CLI in my estate needed something better — a cleaner way to register services, a better error path, a testing helper that made a suite readable — that improvement did not stay in the project. It went into the base, and the base went out to the others.
That is the whole design. The base is small on purpose. It has no opinion about what your tool does. It has a strong opinion about what a command is: something that takes arguments, does work, reports what happened, and leaves.
The folder was created on 6 July 2025, and 2 of my tools were depending on version 1.0.0 of it the same day. That is the tell: it was not built speculatively and then adopted. It was extracted from work that already existed, at the point where copying the same scaffolding between projects had stopped being reasonable.
It spread quickly, because spreading was the entire idea. 8 repositories were on it within 25 days. 10 within 11 weeks.
Repositories Adopting the Base, by Week of 2025
Git came later than any of that. The repository was initialised on 12 November 2025, 4 months in, and published the day after — which is why the version history and the actual history disagree, and why I checked the filesystem rather than trusting the commit log when I sat down to write this.
Those 10 tools do Cloudflare administration. Local DNS and nginx management. Deployment against Coolify. Browser automation. Cost reporting across model providers. Most are private, which is why I am describing them by what they do rather than by name. The public ones are aia, which consults several models in parallel, and the base itself. vssh, my guarded remote-execution tool, is public too and came out of the same instinct — build the operator surface once, properly, and stop rebuilding it.
The dividends were real and they were boring, which is the correct shape for infrastructure dividends. A hardening in one tool showed up in all of them. When I found that a command could print a red error message and still exit 0 — telling the human it failed and telling the shell it worked — the fix did not go into the 16 places in one tool where it had happened. It went into the base, and every tool inherited it.

After 13 months I wanted it rebuilt, not patched

By August 2026 the base was working and I still wanted it gone.
Not because it was broken. Because it had accumulated. Because the exit-code rule I was proudest of had been retrofitted rather than designed in. Because the world it was written for had shifted underneath it: most of the invocations of my CLIs are no longer typed by me. They are issued by agents, reading stdout, stderr, and $? as their only senses.
So instead of patching, I set the terms differently. I gave Claude Fable a single instruction, and I made it deliberately large:
If this were the last CLI framework humanity built — the one still in service in a decade — you now have the chance to make it that.
Design it from there. I did not expect a document back. I expected a plan.

Fable came back with a treaty, and the constraint was that the promises had to be few

What arrived was not a feature list. It was structured as a treaty, split down the middle by a hard wall.
One half was a contract: what every CLI built on this base guarantees to every observer, written as numbered clauses in RFC-2119 language — MUST, MUST NOT, SHOULD, MAY. Twelve families of them. Exit codes. Stream discipline. Machine output. Self-description. Grammar. Environment. Cancellation. Determinism. Performance budgets. Compatibility.
The other half was the authoring surface, which was allowed to grow, and which existed only to make satisfying the contract the path of least resistance.
The reasoning underneath it was the part I found convincing. A design meant to last a decade cannot bet on fashion, because fashion is what expires. It cannot bet on cleverness, because cleverness is what you cannot predict in year 8. It can only bet on the interfaces that have not moved since the 1970s: argument vectors, 3 streams, an 8-bit exit code, environment variables. And it noted the one genuinely new fact — that the majority reader of those interfaces is now a machine that cannot ask a follow-up question.
The clause that ended up organizing everything else was the one it opened with:
One result, many renderings. A command computes a single result. The exit code, the human text, the JSON document and the streaming lines are all projections of that one value. They cannot contradict each other, because there is only one source.
That is the sentence the whole rebuild turns on.
Diagram source
graph LR
    A["execute() returns  
one value"] --> B["exit code"]
    A --> C["rendered text  
stdout"]
    A --> D["JSON envelope  
--json"]
    A --> E["NDJSON stream  
--ndjson"]
    F["logger.error()  
ctx.emit()"] -.-> B
    F -.-> G["events  
stderr"]

Opus 5 and I found the spec was right about the thesis and wrong about 3 things

This is where the work became ours rather than mine.
I brought the spec to Opus 5 and we built it in a day. Not a clean day. The useful parts are the places where the document met the estate and lost.
The spec wanted ctx.args to become a record of named arguments. It is the better design in isolation. It would also have broken every command in every one of the 10 tools, because they all read ctx.args as an array. We kept the array and put the typed arguments on ctx.namedArgs beside it. The rule that decided it was already written in the contract, one clause above: never break a consumer outranks every other value in the repository, including the contract's own completeness.
The spec wanted a command group with no verb to be a usage error. Running a parent command with no subcommand would exit 2. Defensible, and it would have changed the behavior of every script that runs a bare grouping command to see its help. We kept printing help and exiting 0.
The spec assumed streaming and the single JSON document were the same feature. They are not. Streaming a million items in constant memory is the point of one and impossible in the other, because a caller who asked for a single document asked for it to be single. We split the behavior and wrote down which clause governs which.
We also found things the spec could not have known, because they were only visible from the artifact. A test file that ran 0 tests and reported success, having killed the runner partway through. Signal handling that exited 0 on Ctrl-C — an interrupted command reporting that it had succeeded. Two output helpers that joined their lines with a literal backslash-n, so every table came back on one line. A colour helper that, once we replaced the dependency it had wrapped, quietly narrowed its own type signature and broke code that had not changed a character.
That last one is worth sitting with. It was caught by no test either of us wrote. It surfaced in a consumer's typecheck during migration, which is the only place it could have.

The contract only counts because the build fails when a clause has no test

A promise nothing checks is a comment.
So the conformance suite parses the contract file, finds every clause containing the word MUST, and fails the build if one of them has no registered test. You cannot add a promise to this project without adding the thing that proves it, in the same commit.
Conformance Tests by Contract Family
46 normative clauses. 92 tests mapped to them. 184 tests in total.
And none of those conformance tests run against the source. They build the package with its own build script, run npm pack, unpack the tarball, write fixture CLIs that import the unpacked entry point, and spawn them under Node, Bun and Deno — asserting on the exit status and the bytes exactly as a shell would see them.
That shape was not an aesthetic choice. This package once shipped a 65 KB stub. A single "sideEffects": false flag let the bundler tree-shake the router and the exit-code module out of the artifact while their names stayed in the export list. The build exited 0. The source suite stayed green the entire time. Only the artifact was evidence, and nothing was looking at the artifact.

Migrating 7 tools found 3 gates nobody knew were there

We migrated 7 of the 10 CLIs the same day, and the migration is where the design got its actual grade.
The dividend landed immediately and cost nothing: because commands in the old version already returned values — the framework used them only to derive an exit code, then discarded them — every one of those return values became a JSON payload on upgrade day. 7 tools gained machine-readable output without a single command being rewritten.
What we did not expect was the same defect in 3 different tools, none of which knew about each other. Each had a gate in front of the router: a hand-maintained list of valid command names, or a startup step that demanded credentials before anything else ran. In every case the new manifest command — the one that describes the tool's entire surface in a single call, so an agent can learn it without reading source — answered with "unknown command" or "missing token."
One of them kept a second copy of its command list and a hand-written help screen, both of which had drifted from what the tool actually did. Deleting both took its suite from 52 passing with 3 failing to 57 passing with 0. The largest tool in the set has 364 tests, and they passed before and after the upgrade without a source change.
The pattern generalized well enough that it became a written procedure, shipped inside the package itself. It is 9 steps, and the 2 steps that consume the time are the 2 nobody anticipates.

Zero dependencies is the only number that needs no monitoring

The base had 2 runtime dependencies. It now has none.
That was partly aesthetic and mostly arithmetic. On 8 September 2025, an attacker phished the npm account of Josh Junon, maintainer of some of the most depended-upon packages in JavaScript, using a fake domain and a live one-time code. 18 packages were published with malicious versions, including chalk and debug — packages carrying something on the order of 2.6 billion downloads a week between them. The payload was a crypto-clipper. Maintainers caught it and reverted within roughly 2 hours, and the compromised versions were still downloaded about 2.6 million times in that window.
Chalk is one of the 3 libraries the models kept reaching for when I asked them for a CLI.
The base was not affected — it never depended on chalk — and I want to be precise rather than dramatic about that, because it was created 2 months after the incident. The relevance is not that we dodged something. It is that the incident describes the class of risk exactly: every dependency is a decade of someone else's release decisions, and you are trusting an account you do not control. The colour handling that replaced one dependency is about 60 lines. The prompting that replaced the other is about 120. Zero is the only number that needs no monitoring.

What the base gives back now

The version that shipped is 85 KB, unminified, with no runtime dependencies, running on Node, Bun and Deno. Every command built on it gets, with no per-command code:
GuaranteeWhat it means in practice
Honest exit codesAn error reported to a human is reported to the shell
--json and --ndjsonThe value your command returns, in a shape a machine can parse
manifestThe whole tool described in 1 deterministic call, loading nothing
Stream disciplinestdout is payload; every log line is on stderr
Usage errorsExit 2 for "you called me wrong", distinct from 1 for "I tried and failed"
Prompt safetyA prompt with no terminal fails in milliseconds instead of hanging forever
CancellationCtrl-C aborts the command's signal, then exits 130
The thing I keep coming back to is not any single item on that list. It is that the list is now checkable. The README's own example runs as a test against the published tarball, and the numbers quoted in its prose are held to the numbers the suite produces — a rule that caught its first error within a minute of being written, where the page said 87 KB and the artifact was 85.
The base is open source at github.com/light-merlin-dark/merlin-cli, and the contract is a file in the repository rather than a claim on a website.
Four years ago the problem was that every model had a different opinion about what a CLI should be. The answer was never to argue with the opinions. It was to own the base they all build against, and to write the promises down somewhere a build can fail.