Configuration
The configuration system for maltty CLIs. Supports multiple file formats, automatic discovery, Zod schema validation, and a typed config client API.
Supported Formats
Config files are named .<name>.jsonc, .<name>.json, or .<name>.yaml, where <name> is the CLI name passed to cli({ name }) or createConfigClient({ name }).
defineConfig()
Type-safe helper for maltty.config.ts. Used by the @maltty/cli build system.
Config Middleware
Configuration is purely opt-in via the config() middleware from maltty/config. Register it in the middleware array to make ctx.config available in handlers.
Lazy loading (default)
By default, config is loaded lazily -- nothing is read from disk until the handler calls ctx.config.load():
Eager loading
Pass eager: true to load and validate config during the middleware pass, before the handler runs:
Middleware options
Using ctx.config
The middleware decorates ctx.config as a ConfigHandle with a load() method. It returns the load result or null on error:
Pass { exitOnError: true } to call ctx.fail() on error, guaranteeing a non-null return:
Loading with layers
Pass { layers: true } to load() to include layer metadata in the result:
Typing ctx.config
The Zod schema validates config at runtime, but TypeScript cannot automatically propagate the schema type to ctx.config in command handlers (commands are defined in separate files and dynamically imported). Use ConfigType with module augmentation on maltty/config to get compile-time safety:
This keeps the schema as the single source of truth -- ConfigRegistry is always derived from it, so they can never drift apart. Every command handler now sees typed properties on the result.config object returned by ctx.config.load().
You can scaffold this setup automatically:
- New projects:
maltty init --config - Existing projects:
maltty add config
Config Client
The createConfigClient factory (from maltty/config) provides a standalone API for loading, finding, and writing config files outside of the cli() bootstrap.
ConfigOptions
config.find(cwd?)
Find the config file path without loading it.
Returns string | null.
config.load(cwd?)
Load and validate a config file. Returns a Result tuple.
config.write(data, options?)
Validate and write config data to a file.
Discovery Order
When config.load() or config.find() is called, files are searched in this order:
- Custom
searchPaths(if provided) - Current working directory
- Git repository root
The first matching file wins. Files are checked in extension order: .jsonc, .json, .yaml.
Config File Examples
Below are examples of what the config files look like in each supported format.
JSONC
JSON
YAML
Common Patterns
Optional config
When config is optional (not every project has a config file), handle the [null, null] return from config.load():
Writing config from a setup command
Use config.write() to create a config file from user input:
Environment-specific config
Use different config file names for different environments:
Troubleshooting
Config validation fails
Issue: config.load() returns a validation error.
Fix: The Zod schema must match the shape of your config file. Use z.object({}).passthrough() during migration if you need to accept unknown keys temporarily. Check for typos in field names and ensure types match (e.g., a number field should not contain a string in the config file).
Config file not discovered
Issue: config.load() returns [null, null] even though a config file exists.
Fix: Verify the file is named .<cli-name>.<ext> (note the leading dot). The <cli-name> must match the name passed to createConfigClient() or cli(). Supported extensions: .jsonc, .json, .yaml.
YAML parsing error
Issue: YAML config fails to load with a parse error.
Fix: YAML is sensitive to indentation. Ensure consistent use of spaces (not tabs). Verify the file is valid YAML using an online validator.