CLI reference
Every subcommand, every flag, its environment variable and its default — plus what each command writes and what it exits with.
handbook [global options] <command> [command options]Every command writes its result to stdout as JSON and its logs to stderr, so piping works exactly as you would hope:
handbook analyze --source ~/code/api --work work/api | jq .functions`--help` is generated, not written
Every flag below is derived from one settings registry, so handbook <cmd> --help
always lists the flag, its environment variable, its per-command scoped variable and its
default. If this page and --help ever disagree, --help is right — and a drift test
fails the build.
Global options
| Flag | Effect |
|---|---|
-V, --version | Print the version |
-v, --verbose | Debug logging |
-q, --quiet | Errors only — wins over -v |
--env <name> | Select an environment: loads .env.<name>.local and .env.<name> ahead of .env.local and .env, and prefers handbook.config.<name>.yaml. Same as HANDBOOK_ENV |
--env-file <path> | Load exactly this file, bypassing the .env cascade. A missing file is a loud error, not a fallback. Prefer HANDBOOK_ENV_FILE — see the warning below |
--config <path> | Use this config file instead of discovering the nearest handbook.config.yaml |
Global options go before the subcommand:
handbook --env prod -v generate --source ~/code/api --work work/api`--env-file` collides with a Node flag
Node >= 20.6 has its own --env-file, and it pre-scans the whole command line for
it — including the part after the script path, where it does not actually apply the
file. A path that exists passes through to Handbooks untouched, but a path that does
not exist kills the process first:
$ handbook --env-file /gone.env config
node: /gone.env: not found # node, exit 9, before Handbooks ever runsSo the one case the flag promises to report loudly is the one case it cannot report.
HANDBOOK_ENV_FILE does exactly the same thing and cannot be intercepted:
$ HANDBOOK_ENV_FILE=/gone.env handbook config
handbook: error: ENOENT: no such file or directory, open '/gone.env'The flag still works whenever the file is actually there, and it beats the environment variable when both are set.
analyze
Phase 1 only: build the static call graph. No LLM, no key, free.
handbook analyze --source <dir> --work <dir> [--lang <lang>]| Flag | Default | Env |
|---|---|---|
--source <dir> | required | HANDBOOK_SOURCE / HANDBOOK_ANALYZE_SOURCE |
--work <dir> | required | HANDBOOK_WORK / HANDBOOK_ANALYZE_WORK |
--lang <lang> | auto | HANDBOOK_LANG / HANDBOOK_ANALYZE_LANG |
--lang accepts auto or any one of: cpp csharp dart go java kotlin objc
ocaml php python ruby rust scala shell solidity swift typescript zig.
auto detects and merges every language in one pass and is almost always what you want.
Writes phase1/graph.json, functions.csv, graph.dot, dropped-calls.json,
scan-coverage.json.
files counts what was actually read and parsed; filesUnparsed counts what was not, and
every one of those is named with a reason in scan-coverage.json. See
Artifact formats.
{
"language": "multi",
"files": 412,
"functions": 3187,
"edgesKept": 9042,
"edgesDropped": 611,
"filesUnparsed": 3
}generate
The full pipeline. Needs an LLM endpoint for anything past phase 1.
handbook generate --source <dir> --work <dir> [options]Pipeline options
| Flag | Default | What it does |
|---|---|---|
--phase <spec> | all | all · 1 · 2 (=2a+2b+2c) · 2a · 2b · 2c · 3, or a comma list |
--strategy <s> | (work dir's recorded one, else file) | file or member |
--skeleton <path> | — | Your own skeleton.yaml. Required for --strategy member |
--detail <d> | brief | brief or deep card depth |
--synth-mode <m> | oneshot | oneshot, or doctor for the actor–critic repair loop |
--narrate-lang <l> | en | en or zh |
--max-doctor-rounds <n> | 6 | Doctor convergence rounds |
--resume | false | Skip files that already have a completed card |
--refresh | false | Ignore phase-3 caches |
--llm-cache | false | Cache raw LLM replies under <work>/phase3/cache |
Throughput options
| Flag | Default | What it does |
|---|---|---|
--read-workers <n> | 12 | Concurrent card batches |
--read-batch-size <n> | (1 for deep, 8 for brief) | Files per card batch |
--max-chars-per-file <n> | 0 | Truncate each file at n chars; 0 = no limit |
--assign-batch-size <n> | 25 | Cards per assignment batch |
--assign-workers <n> | 12 | Concurrent assignment batches |
--organize-workers <n> | 8 | Concurrent stage-organize calls |
--narrate-workers <n> | 8 | Concurrent narration calls |
LLM options (shared by generate, plan, resync, studio)
| Flag | Default | Env alias |
|---|---|---|
--provider <name> | openai | OPENAI_PROVIDER |
--model <id> | gpt-4o-mini | OPENAI_MODEL |
--base-url <url> | https://api.openai.com/v1 | OPENAI_BASE_URL |
--max-tokens <n> | 16000 | OPENAI_MAX_TOKENS |
--timeout <sec> | 300 | OPENAI_TIMEOUT |
--llm-retries <n> | 6 | — |
--llm-retry-backoff <sec> | 3 | — |
--llm-concurrency <n> | 16 | — |
The API key is never a flag. Set OPENAI_API_KEY (or HANDBOOK_LLM_API_KEY) in the
environment or a .env file. It is rejected in a config file, because config files get
committed.
The extra request body is never a flag either, and is rejected in a config file for the
same reason. Set OPENAI_EXTRA_BODY (or HANDBOOK_LLM_EXTRA_BODY) in the environment: it
merges vendor fields into every request body — {"thinking":{"type":"disabled"}}, for
instance — and because it is free-form, there is no way to tell a tuning field from an
auth field inside it. Model, message and token fields cannot be overridden through it.
--base-url is a flag and is welcome in a config file — a team pointing every checkout
at one shared gateway is exactly what that file is for. A URL that embeds credentials
(https://user:pass@gw.internal/v1) is refused there, and only there; keep the credential
in the environment.
--provider selects the wire format, not the vendor: openai (the default) speaks to
every OpenAI-compatible endpoint, which is most of them; anthropic and gemini are there
for the two that are not.
{
"phasesRun": ["1", "2a", "2b", "2c", "3"],
"nCards": 412,
"nStages": 9,
"nUnassignedFiles": 0,
"nRegisters": 6,
"usage": { "promptTokens": 1840221, "completionTokens": 214880, "totalTokens": 2055101 }
}render
Work dir → markdown, and optionally more. No LLM.
handbook render --work <dir> [--title <t>] [--html] [--html-single] [--agent-site] [--llms-txt]| Flag | Default | What it does |
|---|---|---|
--work <dir> | required | The work directory to render |
--title <title> | System Handbook | Handbooks title in the output |
--out <dir> | <work>/handbook | Where to write |
--html | false | Also the multi-page HTML site, under <out>/html |
--html-single | false | Also one self-contained <out>/handbook.html |
--agent-site | false | Also the agent index + fact tables, under <out>/agent |
--llms-txt | false | Also llms.txt and llms-full.txt |
--source-base-url <url> | — | Link every file card to <url>/<relative path> |
Without --source-base-url the output contains no external URLs at all, which matters if you are
shipping a handbook for a private codebase.
--out is scoped only: its environment variable is HANDBOOK_RENDER_OUT, not a flat
HANDBOOK_OUT, because --out means something different on plan and skill.
skill
Rendered handbook → agent SKILL package. No LLM.
handbook skill --handbook <dir> --out <dir> --name <slug> [options]| Flag | Default | What it does |
|---|---|---|
--handbook <dir> | required | The rendered handbook directory |
--out <dir> | required | Where the SKILL package goes |
--name <slug> | required | Lowercase-hyphen slug; produces <slug>-handbook |
--project <name> | (--name) | Human project name used in the prose |
--work <dir> | — | Adds coverage.json from the phase-2 assignment |
--source <dir> | — | With --work, adds a content hash per file |
--agent-dir <dir> | — | Ships the agent index and its fact tables under references/agent/ |
--lang <l> | en | SKILL.md body language. Frontmatter stays English |
Two refusals worth knowing
--out must not be the handbook directory, or an ancestor of it: the build starts by wiping --out, which
would delete the very thing being packaged. And --lang zh gives you a Chinese body with English
frontmatter — agent runtimes route on the description text, so translating it would silently break skill
selection.
validate
Check a SKILL package. No LLM. Exits 2 on failure.
handbook validate --skill <dir> [--source <dir>]| Flag | Default | What it does |
|---|---|---|
--skill <dir> | required | The skill directory to validate |
--source <dir> | — | Re-hash the live source to detect drift |
Checks structure, the frontmatter contract, index ↔ stage-page consistency, coverage.json
schema, and hash freshness. Errors and warnings go to stderr.
plan
Handbooks-guided change localization. Needs an LLM endpoint. Read-only.
handbook plan --source <dir> --request "<text>" [--handbook <dir>] [--out <file>]| Flag | Default | What it does |
|---|---|---|
--source <dir> | required | The codebase to plan against (never written to) |
--request <text> | required | The natural-language change request |
--handbook <dir> | — | Rendered handbook or skills/<x>/references. Strongly recommended |
--out <file> | (stdout) | Write the plan here |
--max-turns <n> | 30 | Agent turn budget |
Plus the shared LLM options.
Exits non-zero if the planner gave up — it invented tool results, ran out of turns, or
finished with nothing usable — rather than writing an apology that a script would feed
into apply.
apply
Apply a plan's EDIT blocks. No LLM. Exits 2 if anything did not land.
handbook apply --source <dir> --plan <file> [--dry-run] [--backup-root <dir>]| Flag | Default | What it does |
|---|---|---|
--source <dir> | required | The tree to edit |
--plan <file> | required | The plan from handbook plan |
--dry-run | false | Verify only — never writes |
--backup-root <dir> | <source>/.handbook-patches | Where backups go |
{
"ok": true,
"dryRun": false,
"outcomes": [
{ "index": 1, "file": "src/upload.py", "where": "Uploader.send (~88)", "status": "applied", "line": 88 }
],
"changedFiles": ["src/upload.py"],
"backupDir": "/repo/.handbook-patches/2026-08-08T14-05-11-204Z",
"problems": []
}Statuses: applied · created · no-match · ambiguous · file-missing · not-a-file
· unsafe-path · undecodable · skipped.
rollback
Restore a source tree from a patch backup. No LLM.
handbook rollback --backup <dir> [--source <dir>] [--force]| Flag | Default | What it does |
|---|---|---|
--backup <dir> | required | Backup directory containing manifest.json |
--source <dir> | — | Refuse a backup that belongs to a different tree |
--force | false | Restore even files that changed after the patch |
Without --force, a file whose current hash does not match the post-patch hash is
refused — restoring it would silently destroy whatever was done since.
resync
Roll a handbook forward after a code change.
handbook resync --case <dir> --work <dir> [options]| Flag | Default | What it does |
|---|---|---|
--case <dir> | required | Case directory: edited/ + optional plan.md + optional change.diff |
--work <dir> | required | The work directory to roll forward |
--title <title> | System Handbook | Title used when re-rendering |
--no-llm | (LLM on) | Structural refresh only; prose is marked stale |
--no-render | (render on) | Skip refreshing already-rendered outputs |
--corrections <file> | — | corrections.jsonl; its files widen the refresh set |
--detail <d> | (match the existing handbook) | brief or deep for regenerated cards |
--narrate-lang <l> | (match the existing handbook) | en or zh |
Plus the shared LLM options.
Leaving --detail and --narrate-lang unset is the right default: unset means "match what this handbook
already is", so a resync never silently downgrades a deep handbook to brief.
studio
The local web UI. Runs until Ctrl-C.
handbook studio [--port <n>] [--host <addr>] [--state-dir <dir>]| Flag | Default | What it does |
|---|---|---|
--port <n> | 4860 | Port to listen on |
--host <addr> | 127.0.0.1 | Bind address. Containers need 0.0.0.0 |
--state-dir <dir> | $HOME/.handbook-studio | Registry and managed work dirs |
Plus the shared LLM options — Studio resolves them from the same layers as every other
command, so --model and a config-file llm: block both reach its jobs.
Setting --host 0.0.0.0 does not make Studio remotely reachable in any useful sense: the CSRF guard
checks the Host header, so a request naming a LAN IP is refused with 403. See
Studio.
config
Print the resolved configuration and where every value came from. No LLM.
handbook config [--command <name>] [--json] [--check]| Flag | Default | What it does |
|---|---|---|
--command <name> | generate | Show only the settings that apply to this subcommand |
--json | false | Machine-readable output |
--check | false | Validate only; exit 2 if anything is invalid or missing |
handbook config --command generate # a table, with provenance per row
handbook config --json | jq '.settings[] | select(.source.kind == "env")'
handbook config --check # put this one in CIIt shows broken configuration on purpose
Unlike every other command, config does not abort on an invalid value. A missing --source renders as a
visible — unset (required) row rather than taking down the one tool you would use to debug that exact
problem.
Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | An error — invalid configuration, a missing artifact, a failed run. Message on stderr, prefixed handbook: error: |
2 | A check failed: validate found problems, apply did not fully land, or config --check found something invalid |
2 means "the tool worked, and the answer is no". Scripts should treat it differently
from 1.
The pnpm shortcuts
From a clone, each of these builds first and forwards flags straight through:
pnpm analyze --source ~/code/proj --work work/proj
pnpm generate --source ~/code/proj --work work/proj
pnpm render --work work/proj --html --agent-site --llms-txt
pnpm skill --handbook work/proj/handbook --out skills/proj --name proj
pnpm validate --skill skills/proj --source ~/code/proj
pnpm plan --source ~/code/proj --request "…" --out plan.md
pnpm apply --source ~/code/proj --plan plan.md --dry-run
pnpm rollback --backup ~/code/proj/.handbook-patches/<stamp>
pnpm resync --case cases/mycase --work work/proj
pnpm studio
pnpm config:show --command generate
pnpm handbook --help