Troubleshooting
The things that actually go wrong, what the message means, and what to do about it.
Start here, every time
handbook config --command <the-command-that-failed>It prints the active environment, every .env file loaded, the config file resolved, and
one row per setting with where its value came from. Most "it ignored my setting"
problems are answered by that table in ten seconds.
Configuration
source is required: pass --source, set HANDBOOK_GENERATE_SOURCE, or add it to handbook.config.yaml
Exactly what it says — and it lists every way to supply it. Required-ness is checked after all layers have been consulted, so this means none of them had it.
My environment variable is being ignored
handbook config --command generate | grep -i <setting>The FROM column tells you which layer actually won. Usual causes:
- A flag is overriding it. Flags beat everything.
- You set the flat name but a scoped one exists —
HANDBOOK_GENERATE_DETAILbeatsHANDBOOK_DETAIL. - You set an empty value. Empty reads as unset, deliberately.
- You are running from a different directory: the
.envcascade is cwd-only, unlikehandbook.config.yaml, which is discovered by walking up.
llmApiKey must not appear in a config file (it gets committed)
Move it to .env or the shell environment. This refusal is deliberate.
node: /some/path.env: not found, and exit code 9
Not a Handbooks error at all. Node >= 20.6 has its own --env-file flag and pre-scans the
whole command line for it, so it dies on a missing path before Handbooks starts. Use
the variable instead, which nothing can intercept:
HANDBOOK_ENV_FILE=/some/path.env handbook config
# handbook: error: ENOENT: no such file or directory, open '/some/path.env'The flag is fine whenever the file actually exists.
handbook.config.yaml: must contain a mapping of settings, not a list or a scalar
The file parsed as YAML but is not an object at the top level. Check the indentation of the first key.
Analysis
no analyzable files found under <dir>
--source points somewhere with nothing the analyzer recognizes. Check for a typo, and
check that you are pointing at the source root rather than a directory of build output.
handbook analyze --source $REPO --work $WORK -v # see the per-language scanThe file count is far lower than expected
Run with -v and read the [scan] lines. Likely causes:
- A whole language is missing from the list → see Language support.
- Your code is under a directory in the shared skip list (
vendor,build,dist,out,target, …). Point--sourceat the real source root. - Swift on Node ≥ 24 → the adapter refused at discovery. Use
node --liftoff-only.
The file count is far higher than expected
You are scanning node_modules, a vendored tree, or generated code. The common
directories are skipped automatically; anything else needs a narrower --source.
edgesDropped is enormous
Normal for dynamic languages, and not an error — every dropped call is categorized in
phase1/dropped-calls.json rather than guessed at:
jq '.metadata.byCategory' work/api/phase1/dropped-calls.jsonGeneric-tier languages drop more by design. See Analysis fidelity.
A file I know exists has no page in the handbook
Ask phase 1 first — a file that never became facts never becomes a page:
jq '.metadata.byReason' work/api/phase1/scan-coverage.json
jq -r '.files[] | "\(.reason)\t\(.file)\t\(.detail)"' work/api/phase1/scan-coverage.jsonreason | Meaning | What to do |
|---|---|---|
unreadable | the read failed — permissions, a dangling symlink, a race | fix the file or the mode, then re-run analyze |
unparsable | the grammar threw or returned no tree | usually shell + case; see Language support |
partial | it parsed, but with syntax errors | the page exists but is incomplete — read the file itself |
unreadable and unparsable files are deliberately removed from graph.json's
scannedFiles, so no card is written about a file the parser never read and
_coverage.json cannot count it as described. partial files keep their page: the facts
in it are real, just not all of them.
An empty files array means everything parsed. If the artifact is missing entirely, the
work directory predates this record — re-run analyze.
Swift kills the process
Fatal process out of memory: ZoneThe bundled Swift grammar aborts on V8 ≥ 13. The adapter refuses at discovery on such a runtime rather than letting this happen — if you see the abort itself, you are on a code path that bypassed it. Run with:
node --liftoff-only $(which handbook) analyze --source $REPO --work $WORKGeneration
phases 2/3 need an LLM client (set OPENAI_API_KEY or pass --phase 1)
No API key resolved. Check handbook config --command generate — the llmApiKey row will
say — unset (required). For a keyless local endpoint, set OPENAI_API_KEY=EMPTY
explicitly.
The endpoint returns HTML
the endpoint returned an HTML page rather than JSON — this is usually a proxy or gateway login pageA corporate proxy is intercepting the request and returning a login page with a 200.
Fix the proxy, or point --base-url at something reachable.
Cards come back empty
Look at what the model actually said:
ls work/api/phase2/cards/_rejected/
cat work/api/phase2/cards/_rejected/*.txt | head -50Those are replies that produced no usable card. Common causes: a model too small to
follow the schema, a refusal, or truncation. Try --detail brief, a smaller
--read-batch-size, or a stronger --model.
Which files ended up without prose:
jq '.missing' work/api/phase2/cards/_coverage.jsonRate-limit errors, or a very slow run
Lower --llm-concurrency first. Retrying harder against a rate limit spends the same
tokens twice.
handbook generate --source $REPO --work $WORK \
--llm-concurrency 4 --llm-retries 8 --llm-retry-backoff 5The stages make no sense
handbook generate --source $REPO --work $WORK --phase 2b,2c,3 --synth-mode doctorThe actor–critic loop exists for exactly this. If it still fails, author a
skeleton.yaml yourself and pass --skeleton.
work dir was generated with strategy "member" but --strategy file was given
Deliberate. Re-run phase 2b to switch strategies:
handbook generate --source $REPO --work $WORK --strategy file --phase 2b,2c,3another handbook run is already using <work>
A lock. Either a run is genuinely in progress — including a Studio job — or a previous run died hard. Wait, or remove the lock directory named in the message after confirming nothing is running.
Rendering and packaging
<dir> is not a rendered handbook (missing index.md)
--handbook must point at the rendered directory (<work>/handbook), not the work
directory.
outDir must not be the handbook directory or an ancestor of it
The skill build starts by wiping --out. Pointing it at the handbook would delete the
input. Use a separate directory: --handbook work/api/handbook --out skills/api.
validate warns about stale hashes
Working as intended: the source moved since packaging. Roll the handbook forward:
handbook resync --case cases/latest --work work/api
handbook skill --handbook work/api/handbook --out skills/api --name api \
--work work/api --source $REPO --agent-dir work/api/handbook/agentPlanning and applying
planner produced no usable plan (fabrication) after N turn(s)
The model invented ## Tool result sections — it was reasoning on imagined file contents.
Nothing from that run is trustworthy. Use a stronger model.
planner reached the turn limit without producing a plan
Raise --max-turns, or narrow the request. A vague request makes the planner explore
instead of localize.
apply says no-match
The code changed after the plan was written. Re-run plan. Do not hand-edit the
anchor to make it match — the anchor is the safety mechanism.
apply says ambiguous
The old text appears more than once. Re-run plan, or hand-edit the plan to include
more surrounding context in old so it is unique.
EDIT 1: content between the fenced blocks
The old or new content contains a code fence that closed the block early. Open those
blocks with a longer fence:
### EDIT 1
- file: `README.md`
````old
```bash
echo hi
```
````
````new
```bash
echo hello
```
````rollback refuses a file
Its current hash does not match the post-patch hash — someone edited it after the patch,
and restoring would destroy that work. Check what changed, then --force if you are sure.
Studio
403 when opening Studio
You are not using localhost. The CSRF guard checks the Host header, so a LAN IP or a
container name is refused by design. Use http://localhost:4860, or an SSH tunnel:
ssh -L 4860:localhost:4860 user@hostrepo "x" already has a running job
One job per repository at a time, because the artifacts are not safe for concurrent writers. Wait, or cancel the running job from the UI.
Still stuck
handbook <command> -v # debug logging
handbook config --command <cmd> --json # full resolved configuration
cat work/api/run-manifest.json # what the last good run did
ls work/api/phase2/cards/_rejected/ # what the model actually replied
jq '.metadata' work/api/phase1/graph.json # what was actually scanned
cat work/api/phase1/scan-coverage.json # what could NOT be scanned, and whyIf it is reproducible, the artifacts above are exactly what a bug report needs.