Status: design landed alongside the README rewrite (2026-05-09) and the CLAUDE.md cleanup that introduced the three-tier model. This doc captures the rationale so the next maintainer can judge edge cases without re-deriving the framework.
ash is a recursive-development experiment: the project's value depends on agents actually using ash for their daily operations so the ledger captures real friction data, which feeds the next ship's design. This means the docs that nudge agents into ash are not ornamental — they're load-bearing infrastructure for the experiment.
When the project shipped its first verbs, all agent guidance lived in a single CLAUDE.md. As the surface grew (17 verbs, 5 config sections, ledger retention, jail policy, cross-repo report), CLAUDE.md accumulated three classes of content:
- Operational guidance for agents working in this repo (what to use, when, with what gotchas) — its actual job.
- Verb-manual content duplicating
ash help --verb <name>— drift-prone bloat. - Design-principle prose and configuration spec duplicating the README — the wrong audience for that surface.
The cleanup that landed alongside this doc pulled CLAUDE.md back to (1) and split (2) and (3) onto their proper surfaces. This doc explains the model so the split stays clean.
Three surfaces share the agent-facing load. They have non-overlapping owners:
| Surface | Audience | Owns |
|---|---|---|
| README.md | Humans evaluating ash | Pitch, install, verb surface index, design principles, configuration spec, examples, roadmap |
| CLAUDE.md | Agents working in this repo | Switch criteria, hard-won gotchas, session feedback ritual, memory hygiene, bash whitelist |
ash help [--verb <name>] |
Agents needing per-verb args | Authoritative arg schema (defaults, types, descriptions) — never duplicated in markdown |
docs/<topic>.md |
Designers and maintainers | Per-feature design depth (configuration, install, bench, PreToolUse, ledger-cleanup, this doc) |
| Hook denial messages | Agents in any ash init'd repo |
The minimum self-contained nudge to use ash |
The split is not just organizational — each surface has a different failure mode if it bloats:
- README bloat turns away prospective users at the install gate.
- CLAUDE.md bloat buries the load-bearing wisdom under reference-material noise; agents skim and miss the gotcha they needed.
ash helpbloat is impossible by construction — it's generated from the verb schema.docs/<topic>.mdbloat is fine; that's where depth belongs.
The non-obvious failure mode is README/CLAUDE.md duplication: when both copies say the same thing, the maintainer who edits one but forgets the other introduces silent drift. The agent reading CLAUDE.md sees a different reality than the human reading README, and neither knows.
For a normal CLI, "agent docs" are nice-to-have. For ash, they're the lever the experiment depends on:
- The recursive-dev premise depends on agents using ash. No
ashuse → no ledger data → no friction signal → no next-ship design. - The harness's defaults pull agents off
ash. Out of the box, Claude Code prefers its built-inGrep/Glob/Read/Edit/Writetools.ashonly wins when the doc surface (and the hook) actively redirect. - Friction is the product. CLAUDE.md exists to make friction visible, not to hide it. Workarounds it documents should be transparent ("here's what to do until verb X ships") rather than smoothing over a bug.
This is why CLAUDE.md needs more care than e.g. a CONTRIBUTING.md — it's the entry point for the agent loop that drives everything else.
Today's most common drift modes, in order of frequency:
- Verb count and ship number — every ship updates the count somewhere; some places get missed (ASH-56 swept several mismatches at once).
- Per-flag args — a new flag ships in
internal/verbs/<verb>/<verb>.goand updates the help schema; the CLAUDE.md example block doesn't mention it. The audit before this cleanup found four such gaps (grep --no_text,stat --with_meta,test --short,report --top). - Notation drift — early
git diff <flag>examples linger after the verb moved togit --op diff <flag>. - Stale references —
ash killmentioned after the verb was renamed toash stop(ASH-67).
The principle: if a fact can be derived from ash help or git log, don't duplicate it; reference. Concrete applications:
- CLAUDE.md's per-verb sections cite one canonical example + a pointer to
ash help --verb <name>. The full arg list lives in help. - README's verb table is one row per verb, no per-arg detail. Detail belongs in
ash help. - Configuration schema lives in
ash.toml.exampleand README. CLAUDE.md points to both. - Constraints and design principles live in README. CLAUDE.md points there.
Future extension worth considering (not in scope for this round): a CI check that asserts CLAUDE.md's verb-count claim matches ash help, like ASH-42 did for ash usage. File a ticket if drift recurs after this cleanup.
CLAUDE.md should document a fact only when all three are true:
- Born from observable session friction. Born from a real session and documented in commit messages, Linear tickets, or a postmortem comment in the source. Not theoretical.
- Not derivable from source or
ash help. A note that "grephas a--fixed_stringflag" is reference material; help has it. A note that "shell quoting silently corrupts--old_string" is wisdom; you can't infer it from the help schema. - Actionable by an agent without reading additional docs. "Use stdin via
--content -" is actionable. "There's a class of issues with shell quoting" is not — it's punting.
Examples of load-bearing entries that earned their place:
- Shell quoting on
ash edit/ash write(ASH-48, ASH-60, ASH-63) - Daemon stickiness — config changes need explicit restart (ASH-49, ASH-61)
- Path-form semantics differ across verbs (ASH-33 sweep + ship-5-git-status note)
ash read --rangestart vs. end clamping (ASH-57)- Hook redirects
cat/echo/printf/tee+>toash write(ASH-69)
A fact that fails any of the three tests belongs somewhere else: source comments, design docs, or just the help schema.
Hook denial messages tell agents "See CLAUDE.md 'When to prefer ash over bash'." But a repo that ran ash init does not receive a CLAUDE.md from ash init — only the hook config, the gitignore line, and the registry entry (see internal/verbs/initverb/initverb.go).
This is a real UX gap. An agent dropped into a target repo for the first time sees a stream of hook denials with embedded suggestions but no operational doc framing why they should switch or what the gotchas are. The hook's deny message is a nudge, not an onboarding.
The chosen direction (filed as a follow-up Linear ticket in the ash team): ash init embeds a generic agent-guidance template in the binary and writes (or merges into) the target repo's CLAUDE.md/AGENTS.md. The template covers:
- The switch criteria (verbs to prefer)
- Shell-quoting wisdom (load-bearing footgun)
- The session-feedback ritual location (or a target-repo equivalent)
- A pointer back to the ash repo's full CLAUDE.md / docs
The template is generic — no repo-specific paths or verbs that don't yet exist. Idempotency follows the hook-entry pattern: re-running ash init is a no-op; an existing CLAUDE.md gets append-merged with a conflict warning unless --force.
This is not in the scope of the current cleanup. The ticket parks the work with the chosen direction so it's not lost.
When editing CLAUDE.md, watch for:
- Verb-manual content.
ash grep --pattern <re> --path <p> [--glob <pattern>] [--case smart|sensitive|insensitive] ...— that'sash help --verb grep's job, not CLAUDE.md's. Keep one canonical invocation per use-case and point to help. - Design-principle prose. "Robot-first, human-second. Every design decision optimizes for agent token efficiency." That's a README pitch line. CLAUDE.md is operational, not aspirational.
- Marketing tone. CLAUDE.md is for an agent that's already committed to the project; it doesn't need to be sold on the recursive-dev premise. Tight, declarative, action-oriented.
- Dead references.
ash kill(nowash stop), earlygit diff <flag>syntax (nowgit --op diff <flag>), Phase 1 ship numbers in a Phase 2 doc. Sweep these on every verb-shipping PR. - Configuration schema recap.
ash.toml.example+ README §Configuration are canonical. CLAUDE.md references, doesn't recite.
When a new verb ships:
- Always: update the verb's
Argsininternal/verbs/<verb>/<verb>.gosoash help --verb <name>reflects the new schema. - Always: add a one-row entry to README's verb table under the right domain (file system / VCS / build-test / observability / lifecycle).
- Sometimes: add a CLAUDE.md switch-criteria entry only if there's a new use case the agent should reach for the verb in. Don't add a CLAUDE.md entry just because the verb exists.
- Sometimes: add a CLAUDE.md Gotchas entry only if a session note surfaces a non-obvious failure mode. Don't pre-emptively document hypothetical gotchas.
- Update README's "Status:" line and roadmap section when a phase boundary shifts.
When a verb's args change:
ash help --verb <name>updates automatically.- README's verb-table row probably doesn't need to change (it's one-line domain-level, not flag-level).
- CLAUDE.md's switch-criteria example is updated only if a flag mentioned in the canonical invocation changed.
When a session note surfaces friction:
- Decide: is this load-bearing wisdom (per the three-test criteria above)? If yes, add a Gotchas bullet. If no, the session note is the right home — don't pollute CLAUDE.md.
- If it's a verb-level bug, file a Linear ticket and let the fix retire the gotcha.
The goal: most ships should not require a CLAUDE.md edit. The doc gets thinner over time, not heavier.
When this model is working, the following should be true:
- A fresh agent at session start can read CLAUDE.md in under 5 minutes and answer "what should I use ash for?" without consulting README.
- A fresh maintainer can read this doc and answer "should I add this to CLAUDE.md or README?" without further guidance.
ash help --verb <any>is sufficient to know how to invoke that verb; CLAUDE.md and README never claim to be the source of truth for arg defaults.- A target-repo agent can drive ash from hook denials alone (until the embedded-template ticket lands; then they can drive it from a propagated CLAUDE.md).
When you find yourself adding the same content to two surfaces, stop and ask which one owns it. When in doubt, the more constrained surface (README → CLAUDE.md → docs) wins, since narrowing scope is reversible and broadening it costs everyone tokens.