Architecture

One Core, vendored everywhere

The system is built on one decision repeated consistently: draw a hard boundary between what is identical on every machine and what changes with the OS or the operator. Get the boundary right and a single Core can serve every machine with zero special-casing.

Prefer to just read it? Browse the actual config for each layer → — real files, pulled straight from the source repos.

If it changes when the operating system changes, it does not belong in Core.

If it changes when you as an operator change, it does not belong in Core.

Everything left over is Core — and it lives in exactly one place.

The three layers

LayerLives inExamples
Corethis repo, vendored into each OS repo via git subtree — except Windows, which replicates Core nativelyzsh modules, tmux base, nvim, git/delta
OS-nativedotfiles-{MacBook,Windows,Fedora,Arch,…}package manager, clipboard shim, paths
Roledotfiles-Kali · dotfiles-Defenseoffensive engagement (Kali) · defensive detection/hunt (Defense)

The fleet at a glance

Core vendoring topologydotfiles-core is vendored into 8 machine repositories via git subtree (solid edges); dotfiles-Windows mirrors only the Neovim and starship configs via sync scripts (dashed edge). Edges are coloured by live vendoring drift — green when the repo carries Core's current release, orange when it is behind, cyan when the repo replicates Core instead of vendoring it — from each repo's recorded provenance.dotfiles-coreauthored once · vendored via git subtreeMacBookOS-nativev4.10.0FedoraOS-nativev4.10.0ArchOS-nativev4.10.0openSUSEOS-nativev4.10.0AlpineOS-nativev4.10.0GentooOS-nativev4.10.0KaliOS + offensivev4.11.0DefenseOS + defensivev4.10.0Windowsnvim+starshipmirror v4.9.3git subtree — full Core, self-containednvim + starship mirror — sync scripts (Windows host)in sync with Core v4.10.0behind — run a syncreplicated — no Core vendoredCoreOS-nativeHostRole
One Core, vendored into 8 machine repos viagit subtree — each clone is self-contained (no submodule init).dotfiles-Kali stacks a Role layer on its OS layer; dotfiles-Windowscarries no core/ subtree and mirrors only the Neovim and starship configs. Edge colour tracks live vendoring drift — green = carrying Core v4.10.0, orange = behind (run a sync), cyan = replicated, with no vendored release to drift. Each vendoring repo's label shows the release it carries; the host's shows the Core ref its mirror came from.

How an OS repo consumes Core

Each machine repo (except Windows, which replicates Core natively in PowerShell) vendors dotfiles-core under core/ as a git subtree. That physically copies Core in and commits it, so the repo clones and works with no submodule flags — important, since these are public showcase repos people will browse.

# one-time, inside an OS repo:
$ git subtree add --prefix=core https://github.com/dotgibson/dotfiles-core main --squash

# later, to pull Core updates down:
$ git subtree pull --prefix=core https://github.com/dotgibson/dotfiles-core main --squash

Maintainers fan a Core change out to every OS repo in one shot withscripts/sync-core.sh, which prints the exact short SHA each repo receives so a sync is traceable.

How it compares — and when it's the wrong tool

Every dotfiles approach is a trade. This one optimizes for clone-and-go self-containment across many machines and a public, per-machine portfolio — paying a little vendored duplication for it. Here's how that lands against the usual alternatives, each of which wins in its own case.

vs git submodule

A submodule stores a pointer, so a fresh clone is empty until git submodule update --init — the classic "I cloned it and nothing works" footgun. Subtree vendors the actual files, so every machine repo is self-contained and clone-and-go. The cost subtree pays back: a vendored copy in each repo (kept honest by the sync script + a manifest audit).

vs chezmoi / yadm

One repo + per-OS templates is the most DRY answer, and the right move the day you want to collapse the whole fleet into one. This system keeps the multi-repo portfolio instead — each machine is a clean, public, self-contained artifact. Because Core is already plain and OS-agnostic, moving to chezmoi later is a content migration, not a rewrite.

vs GNU stow

stow is a perfect, zero-magic symlink farmer over a single repo — and genuinely simpler if you have one machine. What it has no opinion about is layering or per-OS divergence, so a multi-OS setup drifts into host branches and .stow-local-ignore gymnastics. This system bakes the Core / OS / Role split in and ships its own idempotent symlinker (with backups + a dry-run).

vs a bare $HOME git repo

The git --bare + alias trick is the leanest possible — no symlinks, files live in $HOME. Hard to beat solo. But it has no layer model, no per-OS story, noisy status against your whole home dir, and it is a real footgun on a shared or multi-user box. This trades that leanness for an explicit, auditable structure.

When to reach for something else

A system that can't name its own constraints isn't worth trusting. Don't use this if:

  • You have one or two machines with no real OS spread — this is over-engineered for that; a bare $HOME repo or stow is far less ceremony.
  • You want ONE repo, not a fleet — reach for chezmoi or yadm. (The migration is content, not a rewrite, because Core is already plain.)
  • You can’t stomach any vendored duplication — the multi-repo + vendored-Core model deliberately trades a little duplication for clone-and-go self-containment and a public per-machine portfolio.

The canonical load order

Load order is load-bearing. 00-tools initializes atuin (registering its widget), 10-options runs compinit (fzf-tab + carapace need it), and 35-fzf defines its widgets before 45-pluginsloads zsh-vi-mode, whose init fires the keybinding hook in 40-bindings. Every OS repo’s .zshrc just sources the vendored loader.zsh, which globs $ZSH_CFG/NN-*.zsh, sorts by the numeric prefix, and sources each — the order is the numbering, with no hand-maintained module list:

  1. 00toolsdetection + single init point (zoxide/starship/atuin/mise) — loads first
  2. 05uiterminal-UX primitives (_core_err/ok/hint/confirm/spin), gum-aware
  3. 10optionssetopts + completion system (compinit, cached under $XDG_CACHE_HOME) + zstyles
  4. 15historyHISTFILE (under $XDG_STATE_HOME) + history setopts + secret-ignore
  5. 20aliasesmodern-CLI aliases, each guarded by 00-tools.zsh detection
  6. 25gitcurated OMZ-style git aliases + git_main_branch helper
  7. 30functionscross-OS shell functions (mkcd, extract, serve, …)
  8. 35fzffzf env + zle widgets (Ctrl-T/R, Alt-Z, Ctrl-G) + fif/fbr
  9. 40bindingsvi-mode keybindings (zvm_after_init hook)
  10. 45pluginslightweight plugin loader + pinned plugin list (under $XDG_DATA_HOME)
  11. 50op1Password CLI helpers
  12. 55maintdaily-maintenance surface (maint-install, …)
  13. 60updatethe up updater + once/day "updates available" nudge
  14. 80osthe OS-native layer (os/<platform>.zsh)
  15. 85rolerole stage, one per repo (mutually exclusive) — Kali 85-offensive.zsh · Defense 85-defense.zsh
  16. 99localmachine-specific overrides (99-local.zsh, untracked) — always wins

Two v4 refinements ride this loader. ProfilesCORE_PROFILE=minimal|standard|full — gate the Core band (0069) by number: minimal stops after30-functions, standard after 50-op, andfull loads everything; the OS/role/host fragments (≥ 70) always load, so a lean profile can never drop essential setup. And theXDG split moves mutable state out of the symlinked config tree — history to $XDG_STATE_HOME, the compdump to $XDG_CACHE_HOME, plugins to $XDG_DATA_HOME — while each fragment’s byte-compiled.zwc stays beside it for fast startup.

The Kali role stage

Kali stacks three layers — Core, its apt OS layer, and an offensive role stage (dotfiles-Defense mirrors this on the blue side). Where a plain OS repo’s fragments end … 80-os 99-local, Kali drops in one more —85-offensive.zsh (role band 85), sorting between the two — for engagement scaffolding (scope-first workspaces, an audit-trail logshell, NetExec/BloodHound CE helpers). Engagement data never lives in the repo; it stays under~/engagements, and every tool is for authorized engagements with written rules of engagement only.

The macOS desktop layer

OS-native layers own more than packages and paths. On macOS,dotfiles-MacBook commits a full tiling-desktop setup — window manager, menu bar, and keyboard — themed to Core’s tokyonight palette so the GUI reads as an extension of the terminal, not a separate world.

AeroSpace

i3-style tiling window manager — pure TOML, no SIP disable required, no daemon. alt drives focus, move, resize, and workspaces; its keymap is kept identical to GlazeWM (the Windows host WM) so the tiling workflow is the same on both OSes. JankyBorders rings the focused tile.

SketchyBar

A programmable menu bar themed to tokyonight-storm, matching starship + tmux. Live AeroSpace workspaces on the left; CPU, memory, disk, network, and a click-to-toggle keep-awake on the right — every glyph from the one Nerd Font, no extra deps.

Karabiner

Caps→Ctrl/Esc, and a Tab-hyper layer that mirrors the WM verbs — hyper+hjkl to focus, hyper+1–5 for workspaces — so the whole tiling workflow stays on the home row.

Deep dives

The design decisions above are documented in full on this site. These are the long-form references worth reading next: