Skip to content

Configuration

The options you would otherwise retype on every invocation — which runners to skip, what order to show them in, whether execution is allowed, how files are discovered — live in a settings file instead. The file and the config command that reads and writes it come from @sksizer/detect-runners and are shared between it and waypost: one file, one set of rules, both CLIs. The file is named waypost.config.json even though the library is detect-runners, because way is the name a person actually types.

Lowest precedence first:

| Layer | Where | | ---------------- | ---------------------------------------------------------------------------------------------------------- | | defaults | built in | | user | $XDG_CONFIG_HOME/waypost/config.json, or ~/.config/waypost/config.json when XDG_CONFIG_HOME is unset | | WAYPOST_CONFIG | replaces the user layer’s location — the named file is read instead of the XDG path | | project | waypost.config.json at the project root the directory resolves upward to | | flags | the command line |

XDG is used on every platform, macOS included — a developer with a dotfiles repo expects to find this beside their other tool configuration rather than in a directory they only see in Finder.

WAYPOST_CONFIG=/path/to/file only ever replaces the user layer’s location. The project layer is still read from the project root, still merges on top, and cannot be turned off this way. A relative path resolves against the project root. Naming a file that does not exist is an error rather than a silent empty layer — an env var pointing at nothing is a typo, not a choice.

Objects merge key-wise. Everything else — scalars and arrays alike — replaces wholesale:

user { runners: { disabled: ["cargo"], order: ["moon"] } }
project { runners: { order: ["just"] } }
result { runners: { disabled: ["cargo"], order: ["just"] } }

runners merged key-wise, so disabled survived from the user layer; order was replaced entire rather than concatenated to ["moon", "just"]. Arrays replace because both settings answer “which ones, in what order” — a project that says order: ["just"] means that list. --exclude on the command line replaces runners.disabled for the same reason, and --exclude-path replaces excludePaths.

Schema v1, every key optional:

{
"runners": {
// Runner ids to skip entirely.
"disabled": ["cargo"],
// Preferred display order. Runners not named here follow alphabetically.
"order": ["moon", "just"],
},
// Default for the exec trust gate. --exec / --no-exec still wins.
"allowExec": false,
// git | walk | auto — how files are found.
"discovery": "auto",
// Paths that are not project structure, on top of the built-in list.
"excludePaths": ["crates/manifest-graph/tests/mini-repo"],
// USER LAYER ONLY — a project-layer trust block is rejected the same way a
// project-layer allowExec: true is. Pre-authorizes a class of repositories
// with no per-repository trust marker. See "trust.paths and trust.owners"
// below and [Trust](../trust/).
"trust": {
"paths": ["/home/me/work/**"],
"owners": ["my-org"],
},
}

runners.order reorders the reports behind --json and --list (and the library’s own --table), and the terminal UI’s tab row follows it too. The web view does not: its tab order is a separate, per-browser setting saved by dragging a tab, and that saved order always wins over whatever the server sends (see The web view).

runners.disabled turns a runner off — it is never probed anywhere in this project. excludePaths leaves every runner on and narrows where they look. They answer different questions, and the second is the one a repository usually wants.

The case it exists for is a tree that holds real manifests belonging to nobody: a test-fixture repository, a vendored sample, a scaffolding template. A detector is right to find those and right to report a broken one — they are simply not tasks anyone can run. Disabling the runner would hide them, and would hide the tasks you do have along with them.

{
"excludePaths": [
"crates/manifest-graph/tests/mini-repo", // a fixture repo, not a project
"testdata", // any directory of that name
],
}

Two forms, and deliberately not glob syntax:

  • A bare name matches any single path segment, at any depth. This is the only useful reading for something like node_modules.
  • A value containing a slash is matched from the root, segment by segment. src/app therefore does not also take src/application, and naming one fixture tree does not exclude every directory in the repository named tests/.

Changing this stales the cache. The setting lives outside the tree, so nothing about hashing the repository would notice that it moved; each stored entry records the exclusions it was probed under, and a mismatch re-probes with reason: "excludes".

The library’s own default for allowExec is false — it may be pointed at a repository you did not write, and reading moon means running that repository’s own toolchain configuration. waypost sits in front of it with its own default of true, since running it means standing in your own repository. That default sits above the library’s default but below any configuration file.

allowExec is one exception to the layer order above. waypost.config.json lives inside the repository being scanned, so it may only lower allowExec to false. It can never raise it to true: a repository cannot grant itself permission to run its own code. An allowExec: true in a project file is stripped before the layers merge — the value is not applied, is not attributed to the project layer by config list, and a warning names the file and points at way trust. Only the user file and the command line can turn execution on.

Highest precedence first:

  1. --exec or --no-exec on the command line
  2. allowExec in the user file; and allowExec in waypost.config.json only where it lowers the value to false
  3. waypost‘s own default, true

The library’s allowExec: false default never applies to waypost; its own true answers only where no file answered at all.

way config list and way config get allowExec report waypost‘s own default rather than the library’s, still labelled (default), because the value actually in effect is what those two commands exist to tell you.

This whole list settles the configured value, which is not yet permission to run anything. The configured value is combined with per-repository trust: execution happens only when it is on and the repository has been trusted, or --exec was given for this one run. A true in the user file on its own runs nothing. See Trust.

The same USER-LAYER-ONLY rule as allowExec, but stricter: trust has no “lower” state a project file could honour, so a project-layer trust block is dropped before the layers merge, entirely, with a warning — never partially applied.

{
"trust": {
"paths": ["/home/me/work/**"], // a glob over the resolved repo root
"owners": ["my-org"], // case-insensitive origin remote owners
},
}

This pre-authorizes a whole class of repositories — everything under a directory, or everything cloned from an organization — without a per-repository trust marker for each one. See Trust for the precedence between a marker and this policy, the glob dialect, and the threat model behind matching on trust.owners.

config list [--json] every setting, and the layer it came from
config get <key> one setting's effective value, as JSON
config set <key> <value> [--user|--project] write a setting (default --project)
config unset <key> [--user|--project] remove a setting (default --project)
config path where each layer's file lives
config --help this list

Keys are dotted: runners.disabled, runners.order, allowExec, discovery, excludePaths, trust.paths, trust.owners. A value is read as JSON and falls back to the bare string when that fails, so set discovery git and set runners.order '["moon","just"]' both work and a plain word needs no quoting.

list annotates each value with the layer that supplied it — the question you ask when a setting is not what you expected. The settings themselves go to stdout and the file locations and any warnings go to stderr, so --json stays machine-readable and the plain listing stays greppable. get splits the same way: the value alone on stdout, any unknown-key warning on stderr.

-C (or --dir) reaches config too, so

way config path -C /elsewhere reports that directory’s files rather than the current one’s.

An unknown key in the file is a warning naming the file and the key, and the rest of the file still loads. A file that is malformed, or holds a wrong-typed value, is an error naming the file and the problem — there is no silent fallback, because a setting that quietly did not apply is worse than one that refused to load.

2 means the command as typed cannot run at all — an unknown subcommand, option or setting name, or a value of the wrong type for its setting — every one of those decided from the arguments and the schema alone, with no file read. 1 means a well-formed command that failed against the state it found: a file that will not parse, an unset on a key that was never set, a get with no value anywhere. A script can act on the split: 2 means fix the command, 1 means look at the state.