Configure Netsuke

Updated Sep 02, 2026

Configuration precedence, from lowest to highest, is:

  1. One automatically discovered base winner: user configuration, otherwise system configuration, otherwise built-in defaults.
  2. Project .netsuke.toml.
  3. NETSUKE_ environment variables.
  4. Explicit command-line options.

System and user configuration are discovered from platform conventions rather than merged as two separately named Netsuke layers. On Unix, this means the XDG base directories and the home directory; on Windows it means the application-data directories, such as %APPDATA%\netsuke\config.toml. Automatic discovery chooses one exclusive winner among system configuration, user configuration, and built-in defaults. Netsuke then appends the project .netsuke.toml layer, so project values can override the winner while fields present only in the winner remain available.

An explicit selector bypasses automatic discovery. Selectors are checked in this order:

  1. --config <PATH>
  2. NETSUKE_CONFIG=<PATH>

An explicit file that is missing or invalid causes an error; Netsuke does not fall back to discovery.

When automatic discovery finds no configuration file, Netsuke uses its built-in defaults. When it finds a candidate that cannot be loaded, such as malformed TOML or a file whose extends parent is missing, Netsuke reports the load error. A broken discovered configuration is therefore not treated as absent.

On Windows, Netsuke normalizes alternate spellings of a configuration path, including short and long path forms, before comparing discovered layers. A project .netsuke.toml therefore contributes one layer even when two spellings refer to the same physical file.

Diagnose configuration selection

Pass --verbose to see how Netsuke selected its configuration. Structured events report whether --config, NETSUKE_CONFIG, or automatic discovery won, whether a path was present, and which environment lookups were attempted. Events then identify whether Netsuke uses an explicit file or discovered layers.

During the merge, verbose tracing also reports the defaults, file, environment, and CLI layers as they are applied. File-layer events include a bounded path_hash so operators can correlate a layer with discovery events without recording the raw path. CLI events record only the leaf keys in override_keys; they do not record override values such as paths or host lists. If validation rejects the merged configuration, the event includes the rejected setting in key and a bounded explanation in reason. These events make configuration precedence and rejection decisions auditable without exposing user-supplied values.

If an explicit file cannot be loaded, the warning records failure_kind as Missing or LoadError. Verbose tracing uses only path_hash and path_present; it never exposes a file name or full path. The unkeyed path_hash is only a correlation identifier: it does not protect a guessable path from disclosure.

Configuration tracing is disabled in JSON mode, including when json = true comes from a configuration file. This keeps stderr empty for successful JSON commands and reserves it for the single diagnostic document on failure.

For a terminal human-mode failure in either the early diagnostic-mode preference pass or the full config_merge phase, the configuration load failed event includes bounded operation and error_category fields. JSON mode instead emits the diagnostic document. Passing --verbose additionally emits one final metrics snapshot debug event before Netsuke exits. The snapshot is an in-process diagnostic record, not a metrics listener or a Prometheus endpoint. After a successful configuration merge, verbosity can also come from NETSUKE_VERBOSE or verbose = true in a configuration file. A configuration failure before that merge uses only CLI --verbose.

It includes the bounded configuration-load series:

  • netsuke_config_load_total, with outcome=success or outcome=failure.
  • netsuke_config_load_duration_seconds, with one sample for the startup configuration-load attempt.
  • The phase-level config_load_total and config_load_duration_seconds entries, labelled with phase=diag_mode or phase=merge; the counter also carries the bounded outcome value.
  • netsuke_cli_config_discovery_total, with outcome=success or outcome=error, and netsuke_cli_config_discovery_duration_seconds, which records the cached discovery pass duration.
  • netsuke_cli_path_validation_total, with bounded source=file or source=directory and reason=non_utf8, counts raw CLI path rejections. When parsing exits early for one of these rejections, the counter is not present in a shutdown metrics snapshot.

For example, a missing explicit file reports the actionable error first, then the bounded tracing fields and the snapshot (timestamps and metric values vary):

Configuration file error in 'missing.toml': explicit configuration file not found
ERROR ... configuration load failed operation="diag_mode_resolution" error_category="io"
DEBUG ... metrics snapshot metrics=[...]

The snapshot is available for a configuration failure when --verbose was supplied on the command line. A verbose = true setting in a file that cannot be loaded cannot enable diagnostics because configuration merging has not completed. JSON mode suppresses the tracing and snapshot so stderr remains one machine-readable diagnostic document.

Cached merge API (unstable)

Programs using Netsuke's unstable Rust API can retain the layers from one discovery pass and observe the subsequent merge. Construct CachedMergeInput::new(cli, matches, env, discovered) with the parsed CLI values, an injected ConfigEnvProvider, and DiscoveryOutcome::into_layers(); then pass it to cli::merge_with_cached_file_layers_with_observer(input). The function returns the merge result alongside bounded events; replay those events through MergeObserver, such as TracingMergeObserver. Another caller can provide its own MergeObserver implementation. Observers receive bounded MergeEvent values: layer application and failure states, file path_hash and layer counts, CLI override leaf keys, and validation key/reason fields. Configuration values and raw paths are never included. Ordinary merge_with_config* and merge_with_cached_file_layers calls discard their collected events and do not emit merge tracing.

The observer-based cached discovery and merge flow described here is a beta3 improvement. Published beta2 already provides merge_with_cached_file_layers, but not this observer-based flow.

Bounded configuration metrics

Configuration loading is recorded as two bounded metric series, both emitted in the drained metrics snapshot:

  • config_load_total — a counter with the phase and outcome labels that counts each configuration-loading phase. phase is diag_mode for the early diagnostic-mode resolution or merge for the full configuration merge; outcome is success or failure.
  • config_load_duration_seconds — a histogram with the phase label only, recording each phase's duration in seconds.
  • netsuke_cli_config_discovery_total — a counter with a bounded outcome label of success or error for the discovery pass reused by startup.
  • netsuke_cli_config_discovery_duration_seconds — a histogram recording the discovery pass duration without labels.

Legacy recipe operation metrics

Legacy recipe execution emits two bounded metric series in the drained metrics snapshot:

  • netsuke_runner_legacy_recipe_executions_total counts completed build and Ninja-tool runner operations.
  • netsuke_runner_legacy_recipe_execution_duration_seconds records the full duration of each such operation, including shell validation, graph lowering, Ninja generation, and Ninja invocation.

Both series use the same fixed labels. operation is build or ninja_tool; recipe_shell is posix, powershell, or bash; outcome is success or error; and failure_category is none, manifest, graph, ninja_generation, ninja_io, or other. The ninja_tool value identifies a Ninja-tool operation and does not imply that the tool executes a recipe. Metric labels contain no manifest-controlled or process-controlled values.

The annotated sample configuration lists every key. A small project configuration looks like this:

jobs = 4
verbose = true
locale = "en-US"
json = false
no_input = true
color = "never"
emoji = "never"
progress = "never"
accessibility = "on"
default_targets = ["hello.txt"]

Common environment equivalents include:

  • NETSUKE_JOBS=4
  • NETSUKE_VERBOSE=true
  • NETSUKE_JSON=false
  • NETSUKE_NO_INPUT=true
  • NETSUKE_COLOR=never
  • NETSUKE_EMOJI=never
  • NETSUKE_PROGRESS=never
  • NETSUKE_ACCESSIBILITY=on
  • NETSUKE_LOCALE=en-US
  • NETSUKE_DEFAULT_TARGETS__0=hello.txt
  • NETSUKE_NINJA=/opt/ninja/bin/ninja
  • NETSUKE_WHICH_WORKSPACE=0

NETSUKE_LOCALE selects the interface language; see Choose a language with --locale for how it combines with the flag and the system default.

NETSUKE_NINJA overrides the Ninja executable used by build and clean. Leave it unset to use ninja from PATH, or set another executable name or an absolute path. Empty and non-UTF-8 values fall back to the default.

Diagnose Ninja execution

Pass --verbose to see Ninja subprocess diagnostics on stderr. The Executing Ninja subprocess informational event contains stable fields: operation, ninja_program, arg_count, env_override_count, path_overridden, and suppress_stderr. A debug-level companion event contains the redacted command as Executing command: ..., including the arguments used for the invocation.

Ninja executable resolution also emits a debug event with ninja_program and source. The source is NETSUKE_NINJA for a valid override and fallback when the variable is unset, empty, or non-UTF-8. JSON mode suppresses these tracing events so stderr remains parseable.

NETSUKE_WHICH_WORKSPACE switches off the which() workspace-tree fallback search that runs when a command is not found on PATH. Set it to 0, false, or off (case-insensitively) to disable the fallback; any other value, or leaving it unset, keeps the fallback enabled. A non-Unicode value also disables the fallback and is treated as an explicit opt-out, emitting a warning.

Policy values and parsing

The CLI and configuration use the same policy values, and policy names are matched case-insensitively in both places. The accepted values are:

  • --color and color: auto, always, or never.
  • --emoji and emoji: auto, always, or never.
  • --progress and progress: auto, always, or never.
  • --accessibility and accessibility: auto, on, or off.

Lowercase spellings are used in help and examples. For instance, --color ALWAYS and color = "AlWaYs" select the same explicit colour policy as --color always and color = "always". auto follows terminal and environment detection. always or never makes colour, emoji, or progress behaviour explicit. Accessibility uses on and off for its explicit values.

Netsuke has no interactive mode. It never prompts, and no_input = false is rejected. Pass root --no-input in automation to state that requirement explicitly and make the invocation self-documenting.