Skip to content
stacks
Docs menu

Validation checks

What stacks validate checks in an entry, the word budgets, finding severities, exit codes and the --brief preview.

View as MarkdownOpen in ClaudeOpen in ChatGPT

stacks validate checks stacks and fragments before an agent builds from them, and stacks validate --brief previews the brief a composition renders to. It reads files only and runs nothing from an entry. For how to write an entry that passes, see Write your own stack or fragment.

stacks validate                                         # the bundled registry
stacks validate ~/.config/stacks                        # a registry root (holds stacks/ or fragments/)
stacks validate ~/.config/stacks/stacks/my-api --json   # one entry, as a ValidateReport
stacks validate --brief typescript-cli --sandbox        # the brief a composition renders to
Terminal

A relative path resolves against the directory you ran the command from. With no path, validate checks the bundled registry. The flags are listed under stacks validate.

stacks validate
Terminal
...
fragment docker-deploy
  note     conditional-dead-branch  fragments/docker-deploy/FRAGMENT.md:53: stack=astro-fullstack is reachable only when docker-deploy is added with --with

fragment drizzle-postgres
  note     conditional-dead-branch  fragments/drizzle-postgres/FRAGMENT.md:54: stack=nextjs is reachable only when drizzle-postgres is added with --with
...

14 stacks, 23 fragments: 0 errors, 0 warnings, 20 notes
Next: stacks validate --brief astro-blog --sandbox
Output

Each finding names its severity, a stable check id, the file and line, and a message. Findings are grouped by entry, with registry-wide findings first.

What validate checks

validate runs these passes over every entry under the root:

  1. Loading. The frontmatter decodes against the entry format, the id matches the directory, the kind matches the file name, and no symlinks are present.
  2. Frontmatter rules. Decisions name a default, deps carry no versions, step names are unique and have a layer, and a stack has exactly one task-runner tag and a ## Setup section.
  3. References. Every id in fragments, requires, conflicts and applies_to resolves, and conditional markers are well formed and reachable.
  4. Compositions. Each stack is composed in both presets: alone (with its required and default fragments), with each optional fragment and each fitting app added on its own, then with each compatible pair of those extras. Each composition is checked for conflicts, word budget and verify coverage.
  5. Files. merge paths exist and hold JSON, templates use known variables, git doesn't ignore any shipped file, and shipped config files reference real commands, hooks and imports.
  6. Prose. Pinned versions, dated phrases, version-sensitive config files and setext headings.
  7. Search and dependency cards. Search cards decode, match their entry, are current and fit their size limits. In a registry with allowlist.json, dependency cards are checked against it.
  8. Run ledger. In a registry with runs.json, notes on stacks whose recorded runs are missing, stale or failing.

References resolve through every registry root, the one being validated first. A reference that resolves only in another root is a note naming that root. The bundled registry must be self-contained.

Compositions and coverage

Pairs of two apps, and pairs whose fragments conflict with each other, are skipped. Each addition drops any default fragment it replaces, following the same --without hint new gives. Size rows label each composition with with: one fragment id for a single addition, or two sorted ids joined by + for a pair. An over-budget pair finding includes the validate --brief command that previews it.

Every stack's base composition must verify each layer: install, lint, typecheck and test, plus a serve step with a probe when the stack is tagged web or api (verify-layers). Verify commands must be declared in that composition's commands (undeclared-command), and a fragment's commands need verify coverage (ref-command-coverage).

validate also tries each explicitly applicable fragment beside each stack's defaults when the composition pass hasn't already covered that combination (compose-each). Declared conflicts replace default fragments in either direction; required fragments stay.

References in shipped files

Reference checks read the rendered, merged files for every shipped adapter. Claude Code settings permissions and VS Code launch arguments must name composed commands or commands named in inline code (ref-command). Hook paths must exist in the composed files (ref-hook). Literal localhost ports in settings or VS Code JSON warn when an env variable has a PORT segment (ref-port). A reference file that isn't valid JSON or TOML is one error for that file, and its other reference checks are skipped.

Shipped JavaScript and TypeScript relative imports, and shell source or . targets, must resolve to composed files owned by the same entry or a dependency (import-closure). A fragment may import through everything it requires, transitively; a stack may import through fragments.required. Default and optional fragments are removable, so they don't grant import access. Resolution maps .js to .ts or .tsx, tries .ts, .tsx, .js and .mjs for extensionless imports, and tries index.ts for directories. In shell paths, $CLAUDE_PROJECT_DIR means the project root; another leading variable means the script's directory.

Check ids

The check id is stable, so it's safe to match on in --json output (findings[].check).

Check Severity What it means
schema error The frontmatter doesn't decode, isn't YAML, or is missing
symlink error The entry is or contains a symlink
id-matches-dir error id differs from the directory name
kind-matches-file error kind differs from the file name
decision-default error A decision has no (default ...) choice
dep-version error A deps name carries a version
step-names error Two verify steps share a name
step-layer warning A verify step has no layer
task-runner-tag warning A stack has neither or both of the make and scripts tags
stack-setup-section warning A stack's body has no ## Setup heading
unknown-reference error A referenced fragment or stack doesn't exist
resolved-outside-root note A reference resolves only in another registry root
conditional-syntax error A malformed, nested or unclosed conditional marker
conditional-ids error A marker names an id that doesn't exist
conditional-tag warning A tag= or tag!= marker names a tag no stack declares
conditional-dead-branch warning or note No composition reaches the branch; a note when only --with reaches it
compose error A composition fails: a conflict, a missing id or contradictory flags
optional-fits error An optional fragment doesn't fit the stack that lists it
compose-each error A stack plus an applicable fragment fails to compose
verify-layers error A base composition is missing a required verify layer
budget-body error An entry body is over its word budget
budget-composed error A composed brief is over its word budget
undeclared-command warning A verify step calls a command not in commands
ref-command-coverage warning A fragment's command has no verify coverage
merge-path-missing error A merge path isn't shipped by the entry
merge-json error A merged file isn't valid JSON
template-expression warning A bare Actions expression such as ${{ github }} in a .tmpl file
git-ignored warning Git ignores a shipped file, so it would never reach a commit
ref-command, ref-hook error A shipped config names a command or hook that isn't composed
ref-json, ref-toml, ref-config error A shipped reference file can't be parsed or has the wrong structure
ref-port warning A literal localhost port where an env variable exists
import-closure error A shipped import reaches a file outside the entry's dependencies
pinned-version warning Prose names an exact version such as 1.2.3
dated-phrase warning Prose ties itself to a release: "as of 2026", a month and year, rc, alpha, nightly, canary
version-sensitive-file warning A shipped config file that changes between tool releases; describe it in prose instead
setext-heading warning A heading underlined with === or ---; use ##
search-invalid error A search card doesn't decode
search-orphan error A search card has no matching entry
search-missing warning or note No search card: a warning for bundled entries, a note for yours
search-stale warning or note The card predates the entry's current text. A note for an unreviewed author draft (by: "author", reviewedAt: null), which has no reviewed version to go stale against
search-unreviewed warning Nobody has reviewed the card
search-size, plain-versioned warning Card copy is too long, or plain copy names a version
vocabulary-invalid error A card uses a value outside the search vocabulary
vocabulary-ignored note A non-bundled root tries to redefine the vocabulary
deps-invalid, deps-ambiguous error A dependency card can't be read or isn't on the allowlist, or a name is ambiguous
deps-missing, deps-unreviewed warning A dependency has no card, or its card needs review
deps-review-due note A dependency card's review is over 365 days old
authors-invalid, authors-orphan error A registry's authors.json doesn't decode, or names an entry that isn't in that registry
ledger-schema error runs.json is malformed; the other ledger notes are skipped
ledger-unproven, ledger-changed, ledger-failed note A stack has no sandbox pass, changed since its last pass, or its latest run failed

Only default compositions count for ledger notes, and a missing runs.json is normal.

Word budgets

Budgets count every conditional branch in a body, since any of them can be rendered. For a composed brief they also count the CLI's own text, reported separately as fixed text. Every composed brief is rendered on one fixed context (no missing tools, a fresh repository, the npx invocation), so the counts match on every machine.

Subject Limit (words)
Stack body 2,000
Fragment body 1,000
App body (category: app) 3,000
Composed brief 6,000
Composed brief with an app 9,000

Markdown shipped under files/ and adapters/ is counted and reported but has no limit. When a composition runs over, move optional material into a fragment, move reference material into a shipped doc the body links to, and cut what the agent would do anyway.

Severities and exit codes

Findings are errors, warnings or notes. Only errors fail the command.

Exit When
0 No errors. Warnings and notes don't change the exit
2 Errors (ValidationFailed). The full report is still on stdout, and the last stderr line is Validation failed., or with --json the error envelope whose details carry the counts
2 A path that is neither a registry root nor an entry under stacks/ or fragments/ (NotFound), or --with, --without or --sandbox without --brief
3 A broken config.json with --brief
6 The root can't be read
130 Interrupted

The bundled registry must pass with zero warnings as well as zero errors. Your own entries pass with warnings, though most warnings flag text that will go stale or confuse the agent.

Previewing a brief

--brief <stack> prints only the composed brief to stdout, exactly as new would render it on the fixed budget context. In human mode the word count, fixed-text count and limit go to stderr; agent output keeps stderr empty on success.

stacks validate --brief go-cli --sandbox
stacks validate --brief go-cli --with goreleaser --without ci-github
stacks validate ~/.config/stacks --brief my-api --json
Terminal

--with and --without take ids, repeatable or comma-separated. They and --sandbox work only with --brief. The preview uses only the first configured agent (default claude), ignores the always and preset settings, writes nothing and starts no processes.

--json returns BriefPreview (schema validate-brief): the stack, preset, fragments in composition order, the app, briefWords, fixedWords, limit, composition warnings, the brief text, its fingerprint, and findings for the selected entries. Error findings exit 2 with ValidationFailed and keep the brief on stdout.