--- type: reference title: Dotfiles deployment tier description: How dotfiles/ packages become live configuration — stow-equivalent symlinks, host override packages, and the two laws about theme media and systemd enable symlinks. tags: - dotfiles - deployment - systemd - noctalia timestamp: 2026-10-20 verified: 2026-10-20 --- # Dotfiles deployment tier `dotfiles/` holds home-relative trees and `scripts/deploy.sh` is a stow-equivalent: the repo checkout **is** the deployment, and deploying replaces live files with symlinks into the repo, so `git pull` + `deploy.sh` is the whole propagation story. One directory per tool (`niri`, `ghostty`, `gtk-3.0`, `systemd-user`, …); a package named `zz-$(hostname)` is stowed last and its files replace symlinks coming from base packages. | Mode | Effect | |---|---| | `deploy.sh --check` | dry run — adopts identicals, reports what would be linked, lists divergents, writes nothing. Run this first, always | | `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them | | `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined | Two package shapes are not symlinks: `dconf/dump.txt` is loaded with `dconf load /`, and `systemd-user` units get `daemon-reload` plus `enable --now` only when their `ExecStart` binary resolves on the current machine, so a unit belonging to another box stays inert instead of crash-looping. **The tracked unit is the file, not the directory.** A live directory can be a real directory and still be fully tracked, because every file inside it is a symlink into the repo (`niri` is 9 tracked / 1 box-only; `gtk-3.0` is 3/0). Any audit that classifies by directory mislabels the tree — classify per file with `readlink -f`. The live census and its classifier command are in [home-config-census](/inventory/home-config-census.md). ## Law: media the theme pipeline consumes is content, not config Wallpapers live at `dotfiles/noctalia/.local/share/noctalia/wallpapers/` and are gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every clone of a *configuration* repo carried a media library no diff can review, and the payload only ever grows because deleting a tracked blob from the tree does not delete it from history. The topology is not what one would guess: [Noctalia](noctalia-shell.md) keeps its own library at `~/.local/share/noctalia/wallpapers/` as a **real directory of real files**, so the tracked tree was a duplicate of it and never its source — measured 2026-10-20, when the merge of #41 deleted the repo copies and the desktop did not flicker. Two consequences to know before untracking media: the copy that survives belongs to the application, not to the repo; and untracking deletes the file in every other checkout, `git rm --cached` on the branch notwithstanding. Restore on a fresh box: the library lives in `~/.local/share/noctalia/wallpapers/`, a path `deploy.sh` does not manage. Every filename is `wallhaven_.` and the ID is the whole address — `curl -L -o "https://wallhaven.cc/w/"` into that directory puts the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not. ## Law: never `systemctl --user enable` a unit from a worktree `enable` writes `~/.config/systemd/user/default.target.wants/` as a symlink to the unit path it was handed, and that path is absolute. The heartbeat reaps `wt/*` worktrees older than seven days ([live-sync](live-sync.md)), so a unit enabled from a worktree turns into a dangling symlink while `systemctl --user is-enabled` still answers `enabled` — the unit is dead and the status lies about it. Found on 2026-10-20: `blankpad-desk-autoswitch.service` pointed into the reaped `wt/de-redesign-20261009`, so desk auto-switching had been silently off since the reap. Check the link target before the unit file: ```bash ls -l ~/.config/systemd/user/default.target.wants/ # every target must resolve; a dangling one is a reaped worktree, not a config bug ``` Fix is to relink into the deployment checkout (`~/.omp/dotfiles/…`), `systemctl --user daemon-reload`, then confirm with `is-active` — `is-enabled` proves nothing. ## Law: a generated file must never be a required include `~/.config/niri/config.kdl` ends with `include "noctalia.kdl"` — no `optional=true`. That file is noctalia's palette output (`#ff8c00`, `#f43841`, one re-stamp `2026-10-18 17:36:27`), so it is machine state and correctly untracked. But on a fresh box `deploy.sh` links `config.kdl` first, and niri then refuses to start until noctalia has run once and written the file — the compositor is bricked by the order of operations, not by a bad config. Close it at the include, which survives someone skipping the theme pipeline: mark generated includes `optional=true`, as the `display.kdl` and `switch-events.kdl` lines in the same file already are. ## Desk choice is live state, never config `~/.config/niri/cfg/display.kdl` is a relative symlink to whichever profile is active (`desk-1-home.kdl`, `desk-2-portable.kdl`), written by `desk.sh`. Both profiles are tracked in `dotfiles/zz-blankpad/`; the pointer is not, because which desk the box sits on is not a fact about the repo. A fresh box therefore starts with no output configuration — `config.kdl` includes `display.kdl` with `optional=true`, so niri comes up usable but with default monitors until `desk.sh` is run once. ## Law: generated theme output is never deployed noctalia rewrites palette output in place — `noctalia.*`, `themes/noctalia*`, `colors/noctalia*`, `noctalia-*` (the classes the [census](/inventory/home-config-census.md) proves by same-millisecond mtime). A tracked copy of one is a stale snapshot of a colour scheme, and the failure is quiet: `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` (commit `b3d3df4`, 2026-10-18) sat byte-identical to the live file for two days, harmless, while `--check` cheerfully proposed `ADOPT … noctalia.kdl`. The moment the palette changed, the next sync would have quarantined the live file and restored 2026-10-18's colours — a tool silently reverting a user-visible theme choice. `deploy.sh` now refuses the class (`SKIP … (generated theme output)`), `.gitignore` carries the matching patterns so it cannot be re-added, and `tests/deploy.bats` pins both directions: generated output is never adopted, and an ordinary identical config still is. Untracked in #43, Refs #42. ## Law: a `zz` override older than the file it overrides is a time bomb Stow order means `zz-$HOST` wins, and when both packages carry the same path, sync relinks it *silently* — the script's own comment: "managed file changing owners between packages: silent relink, nothing to quarantine". `dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl` held focus ring `width 2` / `active-color "#e01b24"` from `7b7dbf5` (2026-10-17) while the base package absorbed `b3d3df4` (2026-10-18): `width 4`, `"#ff8c00"`. Live was linked to base, the newer intent, so `--check` printed `RELINK .config/niri/cfg/layout.kdl` on every run and one unattended `deploy.sh` would have reverted the amber focus ring with no quarantine and no warning. The duplicate is deleted in #43. Rule: a `zz` file exists only to differ. When base absorbs the change, delete the `zz` copy in the same commit — and never read `RELINK` lines from `--check` as cosmetic noise; they are the sound of a stale override waiting for someone to run the tool for real.