Handbooks
Reference

Environment variables

Every variable Handbooks reads, the naming rule that generates them, the .env cascade, and which ones must never enter a config file.

The naming rule

Every setting has one camelCase key in the registry. Three names are derived from it by the same transform:

SurfaceFrom readWorkersScoped to generate
Flag--read-workers <n>
EnvironmentHANDBOOK_READ_WORKERSHANDBOOK_GENERATE_READ_WORKERS
Config-file keyreadWorkersgenerateReadWorkers, or nested generate: { readWorkers: }

The scoped form always wins over the flat one. That is what lets you say "narrate in Chinese, but only when generating" without touching anything else.

export HANDBOOK_NARRATE_LANG=en              # everywhere
export HANDBOOK_GENERATE_NARRATE_LANG=zh     # …except generate

A few settings are scoped-only, because their meaning changes per command: --out (HANDBOOK_RENDER_OUT, HANDBOOK_SKILL_OUT, HANDBOOK_PLAN_OUT), --handbook (HANDBOOK_SKILL_HANDBOOK, HANDBOOK_PLAN_HANDBOOK), skill's --lang (HANDBOOK_SKILL_BODY_LANG), and resync's --detail / --narrate-lang (HANDBOOK_RESYNC_CARD_DETAIL, HANDBOOK_RESYNC_PROSE_LANG).

Vendor aliases

Seven settings also accept the names people already have exported:

SettingAlias
llmApiKeyOPENAI_API_KEY
llmProviderOPENAI_PROVIDER
llmModelOPENAI_MODEL
llmBaseUrlOPENAI_BASE_URL
llmMaxTokensOPENAI_MAX_TOKENS
llmTimeoutOPENAI_TIMEOUT
llmExtraBodyOPENAI_EXTRA_BODY

Lookup order is: scoped HANDBOOK_<CMD>_<KEY> → flat HANDBOOK_<KEY> → the vendor alias.

Bootstrap variables

Three settings are resolved before everything else, because everything else depends on them. None of them can be set by the thing they load — an --env key inside handbook.config.yaml would have nothing left to read it.

Variable / flagWhat it does
HANDBOOK_ENV / --env <name>Selects a per-environment .env cascade and prefers handbook.config.<name>.yaml
--env-file <path> / HANDBOOK_ENV_FILELoads exactly that one file, bypassing the cascade. A missing file is a loud error. Prefer the variable: Node >= 20.6 owns --env-file too and pre-scans for it, so a missing path dies as node: <path>: not found (exit 9) before Handbooks runs. The flag wins over the variable when both are set
--config <path>Names one exact config file, bypassing discovery

The .env cascade

With no --env-file, the CLI loads a cascade of .env* files from the current directory, highest precedence first:

#FileWhoScopeCommitted?
1the shell environmentalways wins
2.env.<name>.localpersonalthis environment onlyno (gitignored)
3.env.<name>teamthis environment onlyyes
4.env.localpersonalevery environmentno (gitignored)
5.envteambaselineyes

Rows 2 and 3 only apply when --env/HANDBOOK_ENV names an environment. With neither set, only rows 4 and 5 load.

The whole cascade is "call it in this order, first writer wins", because loading a file never overrides a key that is already set. That one rule is what keeps the shell outranking every file, with no extra logic anywhere.

handbook generate --env prod --source ~/code/api --work work/api
# loads .env.prod.local → .env.prod → .env.local → .env
# and prefers handbook.config.prod.yaml over handbook.config.yaml

The cascade is cwd-only

Unlike handbook.config.yaml — which is discovered by walking up to the git root — .env files are read from the directory you run the command in. .env means "this machine, right now". Run LLM-backed commands from the repo root, or pass --env-file.

What the .env parser accepts

KEY=value, an optional export prefix, blank lines, # comment lines, single- and double-quoted values (quotes stripped), and a trailing # inline comment on an unquoted value. CRLF, LF and bare-CR line endings all work. No multiline values.

An empty value reads as unsetHANDBOOK_TITLE= will not produce an untitled handbook.

Secrets

Two settings are marked secret in the registry — llmApiKey / OPENAI_API_KEY and llmExtraBody / OPENAI_EXTRA_BODY. For both that means:

  • it is never a command-line flag (flags land in shell history and ps output);
  • it is rejected if it appears in a config file, with a message saying why — config files get committed;
  • it is masked in handbook config output.

llmExtraBody is a secret because it is free-form. It merges whatever you put in it into every request body, and gateways do accept auth in the body — so the tool cannot enumerate what is in there, and cannot tell a tuning field from a credential. It has no flag at all; use the environment variable.

llmBaseUrl is deliberately not a secret: a team pointing every checkout at one shared gateway has a legitimate reason to commit it. Only a URL carrying embedded credentials (https://user:pass@gw.internal/v1) is refused in a config file — there and nowhere else.

handbook.config.yaml: llmApiKey must not appear in a config file (it gets committed)
— use .env or the shell environment instead

Docker

The image bakes in HANDBOOK_SOURCE=/src and HANDBOOK_WORK=/work, so you only mount volumes:

docker run --rm -v "$PWD:/src:ro" -v handbook-work:/work handbook:local analyze

Docker's own --env-file layers on top of the toolchain's .env loading — both apply, and an OPENAI_* variable passed that way is visible exactly as a shell export would be. .env* files are never baked into the image; see .dockerignore.

Seeing what actually resolved

handbook config --command generate

prints the active environment, every .env file the cascade loaded, the config file it resolved, and one row per setting with its provenance — flag, env, file or default.

handbook config --check    # exit 2 on the first invalid or missing value

Put --check in CI

A typo'd variable used to mean "silently ran at the default". Now it is a failure with the variable named in the message — which is much cheaper to find in CI than forty minutes into a generation run.

The complete list

Every variable, with its type, default and documentation, is on the Configuration reference page — which is generated from the same registry the CLI reads, so it cannot drift.

.env.example in the repo root is generated from that registry too. Every line in it starts commented out, so copying the whole file is safe.

On this page