Beyond Vibe Coding Patterns for Building Real Software with Claude Code

Designing for an Agent Caller

On this page

Living spine for the deterministic-CLI chapter. Each principle is named alongside the Claude pathology it defends against — that’s the chapter’s unique value, and what makes this design guidance more than a rehash of clig.dev. Status: v0, organized by shared mechanism (paralleling the Part II grouping). Examples are stub-level; expand from the Endless before/after pairs listed in TODO.md before drafting prose.


The inversion

clig.dev (the de-facto consensus) optimizes a CLI for a human at a terminal — composability for shells, helpful failure messages for human eyes, sensible defaults for an interactive session. This chapter inverts the audience: when the primary caller is an LLM agent, the agent reads --help text, guesses past gaps, retries on failure, scrapes whatever output you give it, and has additive bias. Several design choices flip; others stay; a few are entirely new and have no counterpart in the human-first canon.

This chapter zooms into one layer of the broader control stack (see control-stack.md): the CLI as the constraint and standardization surface. CLAUDE.md, skills, and hooks cover what the CLI can’t, and several principles below cross-reference upward when they require an instruction from CLAUDE.md to actually land.


The governing move: pave the cowpath, fence the garden

Every principle below is a specification of one governing move. When an experienced developer designs a CLI for an agent, they do two things at once:

  1. Pave the cowpath. Make the intended path the easiest one — short, conventional flag names; complete --help; a single verb that auto-resolves what would otherwise need typing the right ID at the right subpath; stable output formats Claude can parse on the first try.

  2. Fence the garden. Make the unintended paths costly enough that the agent reaches for the intended path instead — refusals on ambiguous input, refuse-and-redirect stubs for deprecated verbs, gates that block destructive operations without an explicit confirmation flag.

Claude takes the cheapest path that seems to work. If the right path is the cheapest, you get reliability for free. If the wrong path is cheaper, no amount of CLAUDE.md instruction will reliably steer past it — agents under-weight soft cues. So the design move is bidirectional: pave the cowpath, and fence the garden.

The principle groups below sort by which side of this move they live on. Group A and Group G mostly pave. Group B and Group E mostly fence. Groups C, D, and F do both at once.

The image generalizes: paving the cowpath is friction reduction on the path the design intends; fencing the garden is friction increase on the paths that look easy but lead somewhere wrong. The fence works because cows go the easy way — they don’t go through the fence, they go around it, which is exactly the move you want.

There’s a second reason fencing isn’t redundant with paving: the workaround Claude reaches for isn’t pre-verified fit for purpose. When the intended path is expensive, Claude commits to the cheap alternative without checking whether the alternative actually solves the original problem. Without a fence, Claude proceeds confidently into a broken state and discovers the gap only downstream. With a fence, the broken workaround hits a wall, and the error message — if it’s the B3-style prescriptive kind — steers Claude back to the intended path. Paving reduces the rate of workaround attempts; fencing catches the ones that still happen. The two halves of the move are complementary, not redundant.


Prerequisite: getting a CLI in the first place (the non-dev on-ramp)

Everything below assumes a CLI exists to design. For the non-developer reader that assumption is the whole barrier — so the chapter opens by lowering it. This section is the practical bridge; the principles follow.

You don’t need a CLI to start; you need the reflex from Ch 6. The building block is already in hand: the unit move of asking Claude to write code for a task instead of doing it in-context (Ch 6, “the unit move”). A CLI is simply what those scripts become once you notice you keep asking for the same one. The standing habit: when a script you had Claude write is one you’ll run again, have Claude fold it into a CLI command — where it gains a name, a --help, and a stable home instead of being re-improvised each time. One-off scripts stay scripts; the recurring ones graduate.

Two scopes, not mutually exclusive:

  • A general-purpose CLI — one you use across all your Claude work (your personal toolbelt).
  • A project-specific CLI — one that lives in a single project and knows its shape. Most people end up with both. Start with whichever has the first task worth converting.

Language choice (for a non-dev picking one):

  • Python — the best default for most people. Bash is too limited: once a command gets even moderately complex, Bash forces Claude into brittle contortions and it thrashes trying to make the problem fit the shell. Python has the headroom to grow.
  • Go — as good as Python for the author, and better if you want other people to run your CLI (it compiles to a single binary they can install without a runtime). Reach for it when the tool outgrows “just me.”
  • Bash — fine for a three-line wrapper, a trap past that. Don’t build anything with real logic in it.

Two non-negotiable features, from day one:

  1. Every command has --help. It’s how Claude learns your CLI (see A2 — Claude reads --help as the spec). A command Claude can’t introspect is a command Claude will guess at.
  2. A guide command. Once you have a handful of commands, have Claude add a guide command that prints an index of how to use the CLI — effectively dumping a top-level markdown file, then pointing at no more than ~5 section files for detail. This is the agent’s map of its own tooling (see C4/D1 for how to keep that map honest and generated, not hand-enumerated). Worked example: /Users/mikeschinkel/Projects/endless/docs/guide.

Group A — Design toward Claude’s instincts

Claude has strong priors from training data and reflexive behaviors at a shell. Design with them; you get reliability for free.

A1. Name flags toward the guess. Claude’s training data is thick with --json, --format, --quiet, --porcelain, --no-color. Pick those names and Claude reaches for them unprompted. Pick --llm, --bare, --output-mode=2 and Claude won’t reach at all — the convention isn’t there to summon. Defends against: flag hallucination, where Claude invents a plausible-sounding flag and reports success against a flag that doesn’t exist.

A2. Make --help complete and parseable per subcommand. Claude reads --help first and treats it as the spec. Thin help text leaves room for Claude to invent the rest from priors. Every subcommand’s help must list every flag and every accepted value. Defends against: the same flag hallucination, plus the “thin spec, confident execution” pathology that compounds it.

A3. Meet Claude where the reflex already lands. Agents have behavioral reflexes ("--help first, then call"), and those reflexes will beat your documentation. Rather than fight a reflex, instrument it: put the signpost where the reflex lands. Endless’s per-command --help augmentation (prepending an “AGENT — read this before using this command” directive above the normal help) is the canonical example — it intercepts the reflex rather than competing with it. Defends against: every pathology where Claude’s instinctive path bypasses the resource you wrote for it (guide-ignored, README-skipped, CLAUDE.md-forgotten).


Group B — Make wrong moves loud, not silent

Claude’s default is confident forward motion. Anything that lets a wrong move look like a right move compounds — Claude reports success, you discover hours later it never happened, recovery is expensive.

B1. Loud failure on ambiguous input. When the CLI can’t tell what the user means, refuse with a specific error rather than picking the most likely interpretation. A CLI that helpfully guesses teaches Claude that guessing is safe. Defends against: confident wrong assumptions — Claude’s “good enough” choice silently propagating into the rest of the work.

B2. Refuse-and-redirect deprecated verbs; don’t delete them. When you rename a command (task starttask claim), leave the old verb in place as a stub that refuses with a prescriptive message. Deletion lets Claude flail through plausible alternatives based on stale training data; refusal surfaces the rename and steers to the new name. Defends against: stale muscle-memory — Claude reaching for command names from its priors that no longer exist.

B3. Errors that prescribe the next action. Error text is Claude’s action surface: it reads it and tries to do whatever the text implies. Opaque errors send Claude rabbit-holing; errors that say “run X” or “set Y” end the loop in one move. Endless’s session-detection failure today (three concrete remediation paths in the error body) is a good worked example — and a great chapter exhibit, because the error message was the right shape even when the underlying logic had a gap. The RFC 9457 “problem details” model is the canonical reference for HTTP; the same idea applies to CLI stderr. Defends against: additive flailing — Claude piling on retries, environment hacks, and speculative fixes when the error tells it nothing actionable.


Group C — Build a stable contract for the output

Claude reads, parses, scrapes, and reuses what your CLI emits. The default output is the most-used “format” whether you like it or not — and the stability of the contract behind it determines how much Claude can rely on the result.

C1. Format flags are a contract; name them by the format, not the consumer. --json describes the output. --llm describes the caller. The first is conventional and Claude-guessable; the second isn’t, and it conflates two design axes (format vs. consumer profile). Name flags by what they emit, even if you later add a preset for “what an agent wants.” Defends against: output-scraping fragility — when there’s no stable contract, Claude positional-parses human text and breaks on the next reword.

C2. Two-tier flag pattern: conventional shorts + canonical long form. Both --json (short, terse, Claude-guessable) and --format json (long, self-documenting in --help, listed alongside other format values) should exist and alias to the same thing. Humans pick the short; Claude picks the most-conventional; new format values land cleanly via the long form. Defends against: the false trade between ergonomics and discoverability.

C3. Adopt a token-efficient stable format for collection output. For uniform arrays of records — exactly the shape of list, status, next-style commands — emit a token-efficient format like TOON (declare keys once, stream rows). JSON’s braces and repeated keys cost tokens; plain text is unparseable. For single deeply-nested objects, JSON still wins. Defends against: token waste and parse ambiguity in the most common LLM-consumed CLI surface (list output).

C4. Index reference material by the consumer’s arrival key. When an agent arrives keyed by command (“I have spawn, where’s that documented?”), an index organized by section forces it to read every section to find out. Author-keyed indexing fails consumer-keyed lookup. Generate a command → section cross-reference. Endless’s endless guide cross-reference table is the worked example. Defends against: the reverse-lookup gap — agents bypassing well-written docs because the docs aren’t navigable by the key the agent has.

C5. --json is a promise about stderr, not just your formatter. Declaring --json tells the caller that every byte the process emits is machine-readable. But the CLI framework (Commander, argparse, Cobra, clap) parses arguments before your code runs, and on an unknown flag, an unknown subcommand, or a missing required option it prints its own plain-text error and exits — bypassing your JSON path entirely. So the agent gets clean JSON on success and on the errors you wrote, then raw text on exactly the mistake it is most likely to make: guessing a flag wrong. Every mainstream framework exposes a hook to intercept this (Commander’s exitOverride() plus a silenced configureOutput; an argparse subclass overriding error()/exit(); equivalents in Cobra and clap) — but it is off by default and invisible in testing, because your own tests exercise only the error paths you wrote. Route the framework’s parse errors through the same JSON reporter as everything else. Defends against: the green-suite illusion — a passing test suite that proves the JSON contract holds for the paths you authored while it silently breaks on the ones the framework owns. (Source: Emmanuel Paraskakis / Level 250, Agent-Ready CLI Checklist — see references.md.)


Group D — Don’t make Claude the source of enumerable truth

Hallucinated facts are the single largest failure mode. The pattern is the same across many surfaces: Claude enumerates something it shouldn’t (commands, fields, IDs, files) and the result drifts from reality.

D1. Code scaffolds truth; the LLM judges; code validates. When you need a mapping (command → guide section, error → recovery, etc.), let code enumerate the ground truth (walk the command tree; glob the docs), let the LLM judge the semantic mapping (which section explains command X), and let code validate the result against the ground truth before it ships. Never let the LLM enumerate. Endless’s /regenerate-guide workflow + just guide-check is the worked example. Defends against: the broadest category of Claude failure — confidently producing a list of things that don’t exist or omitting things that do.

D2. Point, don’t duplicate; add a drift gate. Where one source explains the canonical behavior, other surfaces should reference it, not copy it. Pair the discipline with a deterministic drift gate (a check that fails CI when a reference goes stale). Defends against: doc rot, drift-induced wrongness, and the contradiction-driven confusion when Claude reads two copies of the same thing that have diverged.

D3. Coverage gaps surface as content signals, not bugs. When the tooling detects “command X has no mapped explanation,” report it as a content gap to fix in the guide, not a missing config file. The CLI then drives content improvement instead of swallowing the gap. Defends against: silent under-documentation, where the missing doc is exactly the one the agent needed.


Group E — Survive Claude’s retry and resume reflexes

Claude retries failed commands, resumes from partial context, and re-enters mid-flow with no memory of earlier state. Anything that assumes single-attempt linear execution is fragile.

E1. Idempotent, re-runnable commands. Every command should be safe to run twice. Claude retries automatically on transient failures; non-idempotent commands turn one retry into duplicate side effects. Defends against: cumulative damage from automatic retry behavior.

E2. Non-interactive by default; explicit confirmation for destructive ops. No TTY prompts in any command Claude is expected to call — Claude can’t answer them and will either hang or guess. Destructive operations (delete, force, overwrite) gate behind explicit flags (--yes, --force) so a casual misinvocation can’t destroy. Defends against: the TTY trap (silent hang on prompt) and accidental destruction (a command Claude reached for that did more than it expected).

E3. Verb-noun consistency across subcommands. When task add exists, task delete should too — not task rm, not delete task. Claude pattern-matches across verbs; inconsistent surfaces invite verb invention. Defends against: verb hallucination, the close cousin of flag hallucination.


Group F — Engineer the agent-environment seam

Detecting and accommodating a non-human caller is its own design problem, with no equivalent in human-first CLI guidance. Several of the principles here have no clig.dev analogue at all.

F1. Directive over suggestion in agent-facing surfaces. “Consider reading the orchestration section” is a suggestion; agents under-weight soft cues. “▸ AGENT — read this before using this command” is a directive. Bindingness of language matters because agents pattern-match on tone, and a hedged hint reads as “optional.” Defends against: the under-weighted soft cue — guidance that’s technically present but practically ignored.

F2. Union-keyed, lazy agent-context detection. Where the CLI needs to detect “this is an agent” — to render an agent-only block, look up a responsible session, etc. — accept any of several plausible signals (env vars like CLAUDECODE, AI_AGENT, CLAUDE_CODE_SESSION_ID; absence of TMUX; process ancestry) and degrade gracefully when one is missing. The current Endless session-detection gap is the worked example: detection routes single-keyed through tmux pane info and blocks when that signal is absent (Claude Desktop), even though three other signals were present. Defends against: the surface-fragility pathology — works in one agent environment, silently breaks in another.

F3. Agent-facing behavior must be human-inspectable. Anything the CLI renders differently for agents (augmented --help, alternate output, special errors) needs a flag that lets a human render the same view on demand (--agent-view and equivalents). You cannot improve what you cannot see, and agent-only paths rot in the dark. Defends against: the unobservable enforcement pathology — rules that are technically enforced but invisibly broken.


Group G — Re-cost the intended path

When the workaround is cheaper than the intended path, Claude routes around the design. The fix isn’t to instruct against the workaround in CLAUDE.md (Claude under-weights soft cues, per F1) — it’s to re-cost the underlying choice so the intended path becomes the easy one. This group is the most direct expression of the paving-the-cowpath move.

G1. Make the intended path the cheapest path. When a workaround is cheaper than the supported path — by even one fewer command, one fewer argument lookup, one fewer directory to remember — Claude routes around the design. Don’t scold; re-cost. In the Endless E-1378 verification case, the intended way to test the new schema-change verb was to populate the per-task sandbox with the project DB and run against it. Claude routed to /tmp + XDG_CONFIG_HOME instead — typing the right task ID at the right sandbox subdirectory was more steps than cp to a flat /tmp path. A single endless sandbox clone-db [<task_id>] verb auto-resolves the path and inverts the friction, making correctness the cheap option. Compounding mechanism: Claude doesn’t pre-verify that the workaround is fit for purpose before committing — in the E-1378 case, the /tmp workaround didn’t even work; the gate refused it. Paving the cowpath reduces the rate of workaround attempts in the first place; fencing the garden (B1, B2) catches the ones that still happen. Defends against: path-of-least-resistance routing — Claude solving the visible friction with a quick workaround rather than the slightly-more-effort supported route.

G2. Defaults are choices Claude makes cold. When a creation step offers options for the resulting state, Claude often picks the default — or the option whose name reads simplest at the moment of choice — without forward-reasoning about what the resulting state will need to support. Set defaults to produce the useful downstream state, not the simplest immediate state. In the Endless E-1378 case, the sandbox-creation step supported initializing with a cloned copy of the project DB or with an empty DB; Claude (effectively) chose the empty-DB option. By verification time the sandbox sat empty — useless for testing a schema-change verb against real data — and that emptiness was the product of an earlier choice Claude had made cold, not of any missing tooling. The right default is the populated state, because that’s what the common downstream task needs. Defends against: the cold-default trap — Claude making creation-time choices that produce a useless resulting state for the common follow-on task, without reasoning forward.

G3. Claude blends flag sets that share conceptual space. --config-dir <path> (Go binary, explicit, long-form) and --db main|sandbox (Python wrapper, short, added later) cover related conceptual ground at different layers. They didn’t get co-designed — --config-dir was the Go binary’s original explicit flag; --db main|sandbox was added later on the wrapper because typing the short form was easier for a human. Claude blended them in its head and threaded XDG_CONFIG_HOME (the env-var equivalent of --config-dir) directly into the binary in a context the gate’s design required --db to satisfy. Sub-point worth naming separately: flag-length ergonomics for humans aren’t ergonomics for Claude. The motivation for the shorter form (less typing) is invisible from Claude’s perspective — Claude doesn’t care about flag length, only about pattern-match with priors. So a short flag added for human typing convenience reads to Claude as just-another-flag-in-the-conceptual-space and gets blended with the longer form. Defends against: cross-layer flag conflation; informs A1 — name toward Claude’s guess (the convention), not toward human typing ergonomics (terseness).


Group H — Force reasoning at the gate (make Claude think, not just pay)

Group G re-costs the wrong path with steps. But friction alone has a failure mode: Claude will pay the cost and still do the wrong thing. When the wrong move is reflexive, adding steps doesn’t stop it — Claude just runs the extra steps by rote. What stops it is forcing Claude to articulate something it can’t articulate falsely without noticing the mismatch itself. The gate that works isn’t “more effort,” it’s “produce a reasoned artifact.” Articulation is where the error becomes visible to Claude.

H1. Gate the rarely-wanted path behind a required justifying artifact — not just an extra step. When you want Claude to take path X rarely and correctly, require a free-text flag on the wrong/over-used path that forces Claude to state its reasoning (--justification, --definition, --rationale). The act of writing the justification makes Claude actually evaluate fit, and evaluation surfaces the mismatch. Distinct from G1: G1 makes the intended path cheaper; H1 makes the unintended path reflective. Reach for H1 specifically when re-costing has failed because the wrong move is reflexive rather than lazy. Defends against: rubber-stamping and the available-tool reflex — Claude taking an action because the tool exists, without checking whether it fits.

Three worked examples from Endless, each showing the escalation from friction → forced reasoning:

  • verb add --definition (the load-bearing anecdote). Goal: task titles must start with a real verb. Instruction alone (“always use a verb”) failed — Claude ignored it. A validation gate (task add shells out to check the first word, records confirmed verbs to a JSONL cache) failed too — Claude would just call endless verb add on a non-verb to force it through. What finally worked: a required --definition flag on verb add. Forced to define the word, Claude notices when it isn’t a verb, and the misuse disappeared. The friction (an extra command) was already there and didn’t help; the definition — the reasoning artifact — is what closed the gap. This is the cleanest illustration in the book of “make Claude think, not just work.”
  • research task --justification. Give Claude a research task type and it files trivial research tasks constantly (see the available-tool reflex, Ch 9), each spawning an anticipatory follow-up todo. Requiring --justification to file a research task collapsed the over-use to just the cases that genuinely needed it.
  • cleans_up link --justification (candidate, not yet shipped). After working an epic, Claude wants to file a bug-fix task outside the epic and link it as a cleans_up instead of parenting it as a child. Candidate fix: require --justification to use cleans_up when the target is an epic (not otherwise) — forcing Claude to argue for the non-child relationship surfaces that parenting was the right move.

The through-line, and the chapter’s takeaway sentence: make the path you want Claude to avoid require more effort than the path you want it to take — and when the wrong path is a reflex rather than laziness, make that “more effort” be reasoning, not just more steps.


Cross-references upward to the control stack

Several principles only land because the layer above the CLI backs them up:

  • A1 (name toward the guess) self-enforces because the convention is in Claude’s training data. CLAUDE.md doesn’t need to instruct.
  • A2 (complete --help) is read by Claude reflexively; no upper-layer instruction needed.
  • A3 (meet the reflex) sits at the boundary — the CLI provides the surface; CLAUDE.md doesn’t need to point at it.
  • F1 (directive over suggestion) works in concert with CLAUDE.md, which should also be directive.
  • C3 (TOON for collections) depends on CLAUDE.md to instruct “use --toon for list output” until the convention is in Claude’s priors.
  • D2 (point, don’t duplicate) is backed by hooks (the drift gate can be a pre-commit hook, not just CI).

The chapter prose should make these cross-references explicit rather than treat the CLI as an island.


Open questions

  • Audience tagging. Part II’s “Anyone can catch this” vs “Needs some code-reading” tag doesn’t map cleanly onto design moves. Reframe each principle with a complementary “recognition” version for the non-developer (what they should expect when choosing/using an AI-aware CLI), or accept this chapter as primarily secondary-audience (working devs)?
  • Placement. Part IV (Production Realities) is the current lean, but Group A/B/C overlap meaningfully with Part II pathologies — each principle could anchor the recovery end of a pathology chapter. Could be a Part II chapter (“the design move that prevents the pathology”), or a paired framework page sitting alongside control-stack.md.
  • Worked examples. All principles need before/after pairs from the Endless code; the guide-augmentation example covers A3 + C4 + D1 + F1, but the rest are stub-anchored. See TODO.md.
  • clig.dev tension. Some principles directly invert clig.dev (e.g., “name flags by format, not consumer” tensions with “let the user say what they want done”). Worth a sidebar comparing the two design targets explicitly.
  • What’s missing? This list isn’t complete. Likely candidates not yet captured: exit codes designed for agents; stdout vs. stderr discipline beyond the JSON-contract slice C5 now covers; long-running vs. fast-completing commands; output ordering stability; concurrency safety; how --help ordering affects what Claude reads.

Found something wrong, unclear, or plainly disagreeable? Open an issue