Config files
The zcli_config plugin loads option defaults from a config file — JSON, TOML, or YAML — with no changes to command code. Your Options structs stay exactly as they are; the plugin fills in unset fields before execute runs.
Enable it in build.zig:
.plugins = &.{
zcli.builtin(.help, .{}),
zcli.builtin(.config, .{}),
},
Discovery
The first match wins:
--config <path>— an explicit file passed on the command line (a global option the plugin contributes)./{app}.config.json|toml|yaml— a project-local file in the working directory{user config dir}/{app}/config.json|toml|yaml— the user-level config ($XDG_CONFIG_HOMEor~/.configon POSIX;%APPDATA%, else%USERPROFILE%, on Windows)
If more than one candidate exists, the first in this order is used and the plugin prints a notice naming the file it chose.
Global and per-command values
Top-level keys apply to every command; a table named after a command scopes values to just that command:
# .myapp.config.toml
verbose = true # global — applies to all commands
[list] # scoped — applies only to `myapp list`
all = true
Keys match option field names. A scoped value beats a global one for its command.
Precedence
Config sits between explicit user input and your struct defaults:
CLI flag > env var > command-scoped config > global config > struct default
A value set on the command line — or read from an option’s env fallback — always wins, even when it happens to equal the struct default; the plugin only fills fields the CLI and env left unset.
Every option type coerces from config. A config scalar is stringified and run through the same value parser the CLI and env use, so bools, all integer widths, floats, enums (including optionals), custom parse types, and arrays / multi-value options (from a config list) all work — no per-type wiring.
Failures are loud, never silent. A malformed config file, an unrecognized extension, an out-of-range number, or an unknown enum variant is skipped with a warning on stderr — the default stays — rather than crashing the CLI or being trusted. A value that won’t parse is never injected.
Config can satisfy a required option and participates in validation and constraints like any other source — a validate hook runs on the final value wherever it came from, and a config value counts as “supplied” for exclusive/requires checks.
Next
- Args & options — the full value cascade
- Plugins — how plugins like this one work