sxcli.devSimple Extensible CLI

Config discovery & hardening

Where config files come from, in what order they merge, and the gates every one of them passes before a single byte is parsed. The theme throughout: a config problem is a loud startup error, never a silent skip — silent misconfiguration is the worst bug class.

The three locations#

Searched in order for an applet whose primary alias is srv; later files override earlier ones field by field:

#LocationUnixWindows
1next to the real binary<binary-dir>/srv-config.<ext>same, .exe resolved
2system config/etc/srv/config.<ext>%ProgramData%\srv\config.<ext> (C:\ProgramData when unset)
3user config (XDG)~/.config/srv/config.<ext>%AppData%\srv\config.<ext>

So a user setting overrides a system setting, which overrides the shipped companion — and arguments and environment override them all (defaults < config files < environment < arguments).

An explicit --config path (or SRV__CONFIG) replaces the search entirely: that one file is the configuration. The single exception is --write-config with a missing target — see Core config & arguments.

What the search will and won't see#

The search only probes the extensions it knows: json plus whatever the accepted format providers claim. It never enumerates directories — so config.json.bak, editor droppings and package-manager leftovers are simply outside its view and can never break startup.

Two files at the same location — say both config.json and config.yaml — are ambiguous, and ambiguity is a startup error, not a precedence rule. --config is checked the same way: if you pass --config app.toml and no registered provider claims .toml, startup fails and says so — the file is never half-read or quietly ignored.

The pinned companion location#

Location 1 is special: it configures whatever ships next to the binary, so it gets the strictest treatment.

  • "Next to the binary" means next to the real binary — the executable path from os.Executable() with every symlink resolved. Busybox-style applet symlinks never relocate the companion: a symlink to the binary sitting in an attacker-writable directory must not get to choose the configuration.
  • The companion itself must be a regular file physically in that directory: it is opened refusing a symlink at the final path component (O_NOFOLLOW, enforced atomically by the kernel — no check-then-open race; the Windows variant rejects reparse points).
  • A symlinked companion is a loud startup error — someone put it there. That includes a dangling symlink: the stat probe follows links and sees nothing, so pinned candidates get an Lstat cross-check that catches the squatter before it can become a live redirect.

/etc and the XDG location are deliberately not pinned — symlink-overlay distros (OpenWrt and friends) keep working — and --config is exempt: an explicit user path is the user's business.

Regular files only#

Every config source must resolve to a regular file. The stat probe — which follows symlinks, so a symlink to a regular file still passes at the unpinned locations — refuses FIFOs before any open could block on them, and gives devices and directories a clean startup error instead of whatever reading one would produce.

The size cap#

A config file larger than the cap — default 1 MiB, which covers any sane configuration — is refused. The size is checked on the same stat, before the file is even opened: an oversized config is never opened, read or parsed. A capped reader underneath is defense in depth against stat races and lying sizes, and it never truncates silently.

Like feature suppression, the cap is a build-time property of the binary: fw.MaxConfigSize(bytes) before Main — a non-positive limit is itself a collected startup violation.

Every gate is loud#

The full list of things that fail startup rather than pass quietly: an unknown key anywhere in a file, a key for a suppressed feature or a run-scoped (dump:"-") field, trailing data after the top-level JSON object, an ambiguous location, an unhandled explicit extension, a symlinked or dangling-symlinked companion, a non-regular file, and an oversized file. If a config file is wrong, the operator finds out at startup — not three weeks later when the "ignored" setting finally matters.

One more gate is on the roadmap (designed, not yet built): refusing group/world-writable configs at the system locations — the read-side sibling of the pinning rules, the same check sudoers and sshd make.