Skip to content

How it works

Two front-ends over one configuration file, and one generated artefact.

loadout CLI

loadout ui

config.yaml

loadout apply

init.zsh

sourced by your shell

The CLI and the web interface both read and write the same configuration file. Neither runs when you open a shell.

LocationHoldsWho owns it
The binaryThe catalog, embedded at compile timeThe release
~/.config/loadout/config.yamlYour particularsYou
~/.config/loadout/local.yamlMachine-specific overrides, never syncedYou, per machine
~/.config/loadout/init.zshThe generated shell fileloadout apply
~/.local/share/loadout/binManaged executables, appended to PATHloadout bin
OS keychainCredential values, and nothing elseYour keychain

The catalog is compiled into the binary with go:embed. There is no catalog directory to install and nothing to keep in sync on disk.

loadout apply does four things:

  1. Load. Read config.yaml, overlay local.yaml on top of it, validate the result. Validation is strict and errors name the file, the key, what was expected and what to do.
  2. Resolve. Filter the catalog for the target platform, drop disabled entries, add entries from enabled packs, and generate the aliases your configuration implies — one per host with shortcut: true, one per workspace, one per clone source.
  3. Render. Run every alias and function through the backend for your shell dialect. The backends produce byte-identical alias lines; only the preamble differs, because bash needs shopt -s expand_aliases where zsh needs nothing.
  4. Write. Compare against the existing file, print the diff, write it.

The output is a single static file. Sourcing it defines aliases and functions and appends the managed bin directory to PATH. It spawns no processes and calls no binaries, so it costs nothing measurable at shell startup.

An alias cannot be a program. gpush and .. are shell state, so a binary can only ever emit text for the shell to evaluate. The alternative design — a shell hook that calls loadout on every prompt, or an eval "$(loadout init)" in your rc file — puts a process spawn in the path of every terminal you open, for a result that is identical every time.

Generating a file once and sourcing it makes that cost zero, and makes the result inspectable: the file is plain text, you can read it, and loadout apply shows you the diff before it writes.

Upgrading the binary does not update your shell.

brew upgrade loadout replaces the binary. It does not touch init.zsh, because that file is generated output that loadout has no business rewriting behind your back — you might have deliberately not applied a change.

So after upgrading, run:

Terminal window
loadout apply

Release notes say so explicitly whenever the shipped catalog changed, and the release process detects that from the golden files rather than relying on anyone remembering.

Requirements, and why nothing is silently dropped

Section titled “Requirements, and why nothing is silently dropped”

Every alias and function declares what it needs — a binary, a path, a section of configuration:

pw:
requires:
- binary: code
- config: workspaces

When a requirement is unmet, the entry is still generated. It is not dropped and not commented out. What happens instead is that loadout doctor reports it, loadout describe explains what to install, and the web interface shows it amber.

That is deliberate. Dropping an alias because a tool is missing means the alias silently disappears on a machine where you have not finished installing things, and you get “command not found” with no explanation. Generating it and reporting the gap tells you the truth.

The generator has a backend per shell dialect. The catalog has per-platform command variants where the underlying tool differs — systemctl on Linux, launchctl on macOS — and entries that make no sense on a platform are not generated there at all.

Correctness across that matrix is pinned by golden files covering all four combinations. A change to a template shows up as a diff in each, which is what a reviewer reads, because that diff is what lands in every user’s shell. More on the traps in Shell portability.