Skip to content

Repo Layout

Jose Miguel Ochoa edited this page Oct 4, 2026 · 5 revisions

Repo layout

A loadout repo is yours to shape. One file at its root, loadout.yaml, says where everything lives; every other file goes where it reads best — including next to the dotfiles it maintains. This page covers the shape; Writing your manifest covers what goes in it.

The live example, josemiguelo/.dotfiles, is one repo for loadout and chezmoi:

.chezmoiroot          "configs" — chezmoi reads only configs/
loadout.yaml          meta, layout, data
configs/              the dotfiles (chezmoi's source), each tool's fragment beside its config
  private_dot_config/tmux/
    tmux.conf.tmpl
    .loadout.yaml     tmux, tpack, tmux-plugins, its outdated source
    .loadout/         the scripts that fragment runs
programs/             programs with no dotfiles: cli/, apps/, brew/, …
maintenance/          scripts with no dotfiles, a folder per concern; lib/ for shared helpers
profiles/             what machines of a kind share (omarchy, macos, fedora)
machines/             one file per real machine, named by hostname
state/                written by loadout, one file per machine

loadout.yaml

meta:
  name: jm's machines
  min-tool-version: "2.1.0"

layout:
  fragments:
    - programs/**/*.yaml
    - maintenance/**/*.loadout.yaml
    - configs/**/.loadout.yaml
  machines: machines
  profiles: profiles
  state: state
  configs: configs

data:
  omarchy: false
  work: false
  kitty:
    opacity: 0.85

loadout.yaml may also hold installers, programs, scripts and outdated sections itself, handy for a tiny repo.

  • fragments lists the files that make up the manifest — programs, scripts, installers and outdated sources. Nothing is discovered behind your back: only what these globs match loads, in their order (path-sorted within each). * matches within a folder, ** any depth, ? one character. A wildcard never matches a name starting with a dot, so .loadout.yaml loads only because the glob spells the dot; .git is never entered.
  • machines, profiles and state are folders. machines and state are required; profiles only when a machine extends one.
  • configs names your dotfiles folder (chezmoi's source), when the repo holds them; loadout then observes and applies them too (Configs).

A bad layout fails in one line, naming the problem: a glob reaching loadout.yaml or a machine file, a path leaving the repo, a folder key with a wildcard.

Paths: relative to the file that wrote them

Every path in a fragment — file, any file: command, a with value marked file: — is relative to that fragment's folder, checked on every load to exist inside the repo. Commands run in that folder too, so a script reads its siblings by name. A tool's fragment can move with its config and nothing inside it changes.

# configs/private_dot_config/tmux/.loadout.yaml
scripts:
  tmux-plugins:
    file: .loadout/plugins.sh                 # next to this file
    check: "file:.loadout/plugins.sh check"

Every command also gets LOADOUT_REPO (the repo's root), LOADOUT_FRAGMENT_DIR (the folder it runs in), LOADOUT_MACHINE and LOADOUT_OS (linux or macos), so a shared helper is one line away: . "$LOADOUT_REPO/maintenance/lib/git-clones-behind.sh".

A program's command written by an installer runs in the installer's folder; a variant's own command or check runs in the program's. That's what lets an installer keep a helper script beside it (programs/brew/).

Fragments beside dotfiles

With chezmoi, a tool's fragment can live in the tool's own config folder: .loadout.yaml and .loadout/ start with a dot, and chezmoi never deploys dot entries that aren't its own special files — so they stay in the repo and never reach $HOME. A change to a tool (its config, its plugins, the script that keeps them in sync) is then one folder and one commit.

A *.loadout.yaml fragment holds named sections: installers, programs, scripts and outdated. Any other .yaml file under programs/ is one program, named by its file name, with its top-level keys as its fields.

Tools with no dotfiles go under programs/ (what to install) or maintenance/<concern>/ (scripts). A tool whose config is a single file in a shared folder (~/.config/starship.toml) keeps its fragment in programs/<tool>.yaml.

Machines and profiles

machines/<hostname>.yaml is a real machine; profiles/<name>.yaml is something machines of a kind share. Both sit directly in their folder — the file name is the name. A machine extends a list of profiles, sets its data, and lists the programs it carries under install_with (see Writing your manifest):

# machines/GFT-GJD4Y36T5V.yaml
extends: [macos]

data:
  zscaler: true
  work: true

data: one place for per-machine settings

loadout.yaml's data: declares every setting a machine may have, with its default; machines and profiles override them under their own data:. A tool's settings nest under its name, as kitty does above. A key nobody declared, or of another kind, is a load error. loadout explain shows a machine's merged settings.

Dotfile templates read the same settings, so the hostname never appears inside a template — only as a file name. The live repo's chezmoi templates branch on them through a small partial that merges loadout.yaml, the machine file and its profiles exactly as loadout does:

{{ if (includeTemplate "machine" . | fromJson).omarchy }}
dynamic_background_opacity yes
{{ end }}
background_opacity {{ (includeTemplate "machine" . | fromJson).kitty.opacity }}

Live: configs/.chezmoitemplates/.

Configs

With configs set, loadout reads your dotfiles through chezmoi, using that folder as chezmoi's source. Each top-level config is one entry: a folder under ~/.config is named after the folder (tmux, nvim), anything else after its first path segment (.zshenv, .local). What exists is chezmoi's call: .chezmoiignore leaves a config out on a machine, and nothing is opted in.

$ loadout status
…
  CONFIG                STATUS
  ✔  kitty              applied
╭───────────────────────────────╮
│ ✘  zsh                drifted │
│      .config/zsh/.zshrc       │
╰───────────────────────────────╯

Drifted means loadout apply would change those files: you pulled a change, or edited one here.

$ loadout apply zsh          # write that config (no names: all of them)
$ loadout explain zsh        # its files, and its last verdict
$ loadout diff               # every machine's configs side by side

On the home screen, the configs row opens the same list with the drifted ones ticked; enter applies them.

apply, sync and the configs row stop when a file they'd replace was edited on this machine, and name it. Keep your edit with chezmoi re-add <file>, or take the repo's with loadout apply --force. Without chezmoi on PATH, the configs are not checked, and status says so once.

State

state/<machine>.json is what loadout observed on that machine — written by status, committed by sync, compared by diff. Nothing hand-edited goes there.

Clone this wiki locally