Quarantine what the gate could not see; delete what the repo only pretended to own #41

Merged
dank merged 5 commits from fix/40-dotfiles-hygiene into main 2026-10-20 17:02:53 -07:00
Owner

Quarantine what the gate could not see, and delete what the repo only pretended to own

Found while answering "what other userspace configuration belongs in this repo". Five atomic commits.

1. A live credential had already escaped — 84dd20b

agent/memories/--home-dank-Projects-serverAppConfigs--/learned.md carried, in plaintext, the Caddy basicauth bcrypt hash, the password behind it, and the Pi-hole internal password. That file is tracked: the values are in git history on every machine that has ever pulled this repo, and in every agent context window that has read the bucket. Redacted; all operational knowledge kept (which auth layer actually fails, how to reset, that the CLI cli_pw hash is what CivetWeb validates, hot-reload via the Caddy file watcher).

Action for you, not for this PR: both credentials need rotation on Dankclaw. Redaction does not un-leak history, and this PR deliberately does not rewrite history. ~/.config/secrets/ (four files) and frank-job-hunt.env are untouched and stay out.

2. The gate was reading the wrong files — 04f9313

.githooks/pre-commit scanned only *.md *.yml *.yaml *.json *.toml *.sh *.ts *.js. A postgres.env, a .container quadlet, an .ini, or an extension-less dotfile — exactly what the dotfiles tier is made of — was never read. git check-ignore confirmed the other half: *.env was not quarantined, so containers/systemd/postgres.env was stageable.

Shapes now scan the whole staged diff (bcrypt hashes and Authorization: Basic added); credential assignments scan non-prose files only, because the auto-learned memory tier is prose about credentials — 7 such hits measured on the tracked tree — and a gate that fires there is overridden within a week. Four invariants, each pinned:

  • Fail closed. My first rewrite passed -I to git diff, which is invalid there; the empty file list made every secret commit pass. A blind gate looks exactly like a clean tree.
  • Only added lines can leak. The gate then blocked the redaction commit, because the diff still contained the removed password. A gate that blocks removing a secret holds the leak hostage.
  • It can commit itself. Fixtures must look like credentials, so the exemption is two exact paths (.githooks/pre-commit, tests/secret-gate.bats) — not tests/ — and it prints what it exempted. A secret parked in tests/other.bats is still caught, and that is a test too.
  • Pointers are not secrets. …_FILE=, …_PATH=, ${VAULT}, CHANGEME pass, so the gate stays worth obeying.

tests/secret-gate.bats: 15 black-box tests — the hook is copied into a scratch repo and git itself invokes it.

3. Tracked fiction, and 80 MiB of wallpaper — 411b32f

  • home/dank/.omp/dotfiles/** (3 files) duplicated niri/cfg/autostart.kdl, keybinds.kdl and swayidle-lock.service under a prefix deploy.sh never walks. Live package deployed and symlinked; the copy asserted nothing. → .AGENTTRASH/20261020-tracked-debt/.
  • .pre-pull-backup/** (2 files) — a local backup in history, which .gitignore itself forbids. → same.
  • 23 wallpapers, 80.4 MiB, untracked but left on disk: ~/.local/share/noctalia/wallpapers symlinks into that directory and the theme reads it, so deleting them would have gone black. Filenames carry wallhaven IDs, so a fresh box restores with one curl per file + deploy.sh.
  • New omp-docs/tools/dotfiles-deploy.md — nothing documented deploy.sh at all. It owns the mode contract, zz-$(hostname) overrides, the dconf/systemd-user special cases, and two laws: theme media is content not config; and systemctl --user enable snapshots an absolute path, so enabling from a worktree means the seven-day reap silently kills the unit while is-enabled keeps lying. That second one was live: blankpad-desk-autoswitch.service pointed into the reaped wt/de-redesign-20261009, so desk auto-switching had been off since the reap. Relinked to dotfiles/zz-blankpad/ + daemon-reload; left inactive on purpose — starting it mid-session applies a desk profile.

4. The census described another box — 8628060

inventory/home-config-census.md (dated 2026-08-28, unverified, every path /home/dank) was still advertised as the pruning session's input. Rebuilt from live data, per file (the tracked unit is a symlink: niri 12/1, gtk-3.0 3/0, lazyvim = whole-dir symlink), with the classifier command embedded so it gets rerun instead of trusted. The generated/authored split is proven by mtime: 24 noctalia* palette files written 2026-10-14 19:05:40.330…40.813 in one pass, with later single re-stamps — generation, never authorship, so the source palette stays tracked and the expansion never does. Prune list extends the expected 14 with pywalfox, qt5ct, qt6ct, turborepo.

fib-watchdog.{service,timer} is recorded as owned by the bench repo, not adopted here — its ExecStart/WorkingDirectory are that project's internals. It is flagged as a reinstall risk (is-active → inactive, this box holds the only copy), which is the honest status rather than a second owner.

Verification

bats tests/ 62/62 (15 gate + the two real-bundle lint gates added in #39), kb-lint.sh omp-docs → 0 violations, kb-coverage.py → 0 issues, deploy.sh --check → 0 actions. Gate suite runs the hook through real git commit in a scratch repo, so it proves behaviour, not plumbing.

Addendum — the fifth commit, 9ebf5fe

Answering "does this repo still track my noctalia and niri settings?" falsified two rows
of the census page committed in 8628060: niri is 9 tracked files, not 12; noctalia is
3 authored files (all three live symlinks) against 2672 files of app state and
noctalia's downloaded catalog, not 2 | 0. The page prints its own classifier command and
that command never reproduced the old numbers, which is the only reason they were wrong
rather than merely stale — the row was written from a remembered figure instead of the
measurement.

The probe also found a third piece of tracked debt in the same class as .pre-pull-backup:
dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded, a retired output config
committed 2026-10-09, five days before .gitignore grew .AGENTTRASH/ — tracked-then-ignored,
so the ignore rule cannot reach it and git rm --cached is the only repair. Untracked; the
working copy joined today's .AGENTTRASH set.

Two laws came out of the same probe, both fresh-box failures rather than style notes:

  • ~/.config/niri/config.kdl ends with include "noctalia.kdl", not optional=true.
    That file is noctalia's palette output, so it is correctly untracked — and correctly
    absent on a fresh box, where niri then refuses to start until the theme pipeline has run
    once. The fix belongs at the include, matching the display.kdl and switch-events.kdl
    lines in the same file.
  • ~/.config/niri/cfg/display.kdl -> desk-1-home.kdl is the desk pointer written by
    desk.sh. Tracked profiles, untracked pointer: right split, undocumented, so a fresh box
    boots with default monitors and nobody can say why.

Out of scope

No history rewrite, so the redacted blobs stay in .git — which is exactly why rotation is mandatory. Adopting the nine hand-authored box-only tool configs (git, bat, lazygit, micro, flameshot, autostart) was not selected, so it is not here; the census records them with authorship evidence for whenever you want it.


cwd: /home/blank/.omp/wt/wt-20261019-125509-7c8a99d · session: 01a155bb-6474-7366-be9d-629f74af0e05
Build worktree: ~/.omp/wt/wt-dotfiles-hygiene off origin/main. Refs #40

# Quarantine what the gate could not see, and delete what the repo only pretended to own Found while answering "what other userspace configuration belongs in this repo". Five atomic commits. ## 1. A live credential had already escaped — `84dd20b` `agent/memories/--home-dank-Projects-serverAppConfigs--/learned.md` carried, in plaintext, the Caddy basicauth **bcrypt hash**, the **password behind it**, and the **Pi-hole internal password**. That file is tracked: the values are in git history on every machine that has ever pulled this repo, and in every agent context window that has read the bucket. Redacted; all operational knowledge kept (which auth layer actually fails, how to reset, that the CLI `cli_pw` hash is what CivetWeb validates, hot-reload via the Caddy file watcher). **Action for you, not for this PR:** both credentials need **rotation** on Dankclaw. Redaction does not un-leak history, and this PR deliberately does not rewrite history. `~/.config/secrets/` (four files) and `frank-job-hunt.env` are untouched and stay out. ## 2. The gate was reading the wrong files — `04f9313` `.githooks/pre-commit` scanned only `*.md *.yml *.yaml *.json *.toml *.sh *.ts *.js`. A `postgres.env`, a `.container` quadlet, an `.ini`, or an extension-less dotfile — exactly what the dotfiles tier is made of — was **never read**. `git check-ignore` confirmed the other half: `*.env` was not quarantined, so `containers/systemd/postgres.env` was stageable. Shapes now scan the whole staged diff (bcrypt hashes and `Authorization: Basic` added); credential **assignments** scan non-prose files only, because the auto-learned memory tier is prose *about* credentials — 7 such hits measured on the tracked tree — and a gate that fires there is overridden within a week. Four invariants, each pinned: - **Fail closed.** My first rewrite passed `-I` to `git diff`, which is invalid there; the empty file list made every secret commit pass. A blind gate looks exactly like a clean tree. - **Only added lines can leak.** The gate then blocked the redaction commit, because the diff still contained the *removed* password. A gate that blocks removing a secret holds the leak hostage. - **It can commit itself.** Fixtures must look like credentials, so the exemption is two exact paths (`.githooks/pre-commit`, `tests/secret-gate.bats`) — not `tests/` — and it prints what it exempted. A secret parked in `tests/other.bats` is still caught, and that is a test too. - **Pointers are not secrets.** `…_FILE=`, `…_PATH=`, `${VAULT}`, `CHANGEME` pass, so the gate stays worth obeying. `tests/secret-gate.bats`: 15 black-box tests — the hook is copied into a scratch repo and git itself invokes it. ## 3. Tracked fiction, and 80 MiB of wallpaper — `411b32f` - `home/dank/.omp/dotfiles/**` (3 files) duplicated `niri/cfg/autostart.kdl`, `keybinds.kdl` and `swayidle-lock.service` under a prefix `deploy.sh` never walks. Live package deployed and symlinked; the copy asserted nothing. → `.AGENTTRASH/20261020-tracked-debt/`. - `.pre-pull-backup/**` (2 files) — a local backup in history, which `.gitignore` itself forbids. → same. - 23 wallpapers, **80.4 MiB**, untracked but **left on disk**: `~/.local/share/noctalia/wallpapers` symlinks into that directory and the theme reads it, so deleting them would have gone black. Filenames carry wallhaven IDs, so a fresh box restores with one `curl` per file + `deploy.sh`. - New `omp-docs/tools/dotfiles-deploy.md` — nothing documented `deploy.sh` at all. It owns the mode contract, `zz-$(hostname)` overrides, the dconf/systemd-user special cases, and two laws: theme media is content not config; and `systemctl --user enable` snapshots an absolute path, so enabling from a worktree means the seven-day reap silently kills the unit while `is-enabled` keeps lying. That second one was live: `blankpad-desk-autoswitch.service` pointed into the reaped `wt/de-redesign-20261009`, so desk auto-switching had been off since the reap. **Relinked** to `dotfiles/zz-blankpad/` + `daemon-reload`; left `inactive` on purpose — starting it mid-session applies a desk profile. ## 4. The census described another box — `8628060` `inventory/home-config-census.md` (dated 2026-08-28, `unverified`, every path `/home/dank`) was still advertised as the pruning session's input. Rebuilt from live data, per **file** (the tracked unit is a symlink: `niri` 12/1, `gtk-3.0` 3/0, `lazyvim` = whole-dir symlink), with the classifier command embedded so it gets rerun instead of trusted. The generated/authored split is proven by mtime: 24 `noctalia*` palette files written `2026-10-14 19:05:40.330…40.813` in one pass, with later single re-stamps — generation, never authorship, so the source palette stays tracked and the expansion never does. Prune list extends the expected 14 with `pywalfox`, `qt5ct`, `qt6ct`, `turborepo`. `fib-watchdog.{service,timer}` is recorded as **owned by the bench repo**, not adopted here — its `ExecStart`/`WorkingDirectory` are that project's internals. It is flagged as a reinstall risk (`is-active` → `inactive`, this box holds the only copy), which is the honest status rather than a second owner. ## Verification `bats tests/` **62/62** (15 gate + the two real-bundle lint gates added in #39), `kb-lint.sh omp-docs` → 0 violations, `kb-coverage.py` → 0 issues, `deploy.sh --check` → 0 actions. Gate suite runs the hook through real `git commit` in a scratch repo, so it proves behaviour, not plumbing. ## Addendum — the fifth commit, `9ebf5fe` Answering "does this repo still track my noctalia and niri settings?" falsified two rows of the census page committed in `8628060`: niri is **9** tracked files, not 12; noctalia is **3** authored files (all three live symlinks) against **2672** files of app state and noctalia's downloaded catalog, not `2 | 0`. The page prints its own classifier command and that command never reproduced the old numbers, which is the only reason they were wrong rather than merely stale — the row was written from a remembered figure instead of the measurement. The probe also found a third piece of tracked debt in the same class as `.pre-pull-backup`: `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded`, a retired output config committed 2026-10-09, five days before `.gitignore` grew `.AGENTTRASH/` — tracked-then-ignored, so the ignore rule cannot reach it and `git rm --cached` is the only repair. Untracked; the working copy joined today's `.AGENTTRASH` set. Two laws came out of the same probe, both fresh-box failures rather than style notes: - `~/.config/niri/config.kdl` ends with `include "noctalia.kdl"`, **not** `optional=true`. That file is noctalia's palette output, so it is correctly untracked — and correctly absent on a fresh box, where niri then refuses to start until the theme pipeline has run once. The fix belongs at the include, matching the `display.kdl` and `switch-events.kdl` lines in the same file. - `~/.config/niri/cfg/display.kdl -> desk-1-home.kdl` is the desk pointer written by `desk.sh`. Tracked profiles, untracked pointer: right split, undocumented, so a fresh box boots with default monitors and nobody can say why. ## Out of scope No history rewrite, so the redacted blobs stay in `.git` — which is exactly why rotation is mandatory. Adopting the nine hand-authored box-only tool configs (`git`, `bat`, `lazygit`, `micro`, `flameshot`, `autostart`) was not selected, so it is not here; the census records them with authorship evidence for whenever you want it. --- cwd: /home/blank/.omp/wt/wt-20261019-125509-7c8a99d · session: 01a155bb-6474-7366-be9d-629f74af0e05 Build worktree: `~/.omp/wt/wt-dotfiles-hygiene` off `origin/main`. Refs #40
dank added 4 commits 2026-10-20 16:49:15 -07:00
The auto-learned memory tier for serverAppConfigs carried, in plaintext, the
Caddy basicauth bcrypt hash for dankuser, the password behind it, and the
Pi-hole internal password. The file is tracked, so all three are in git
history on every machine that has pulled this repo, and in every agent
context window that has read the bucket.

Operational knowledge preserved in full (which auth layer fails, how to
reset, that the CLI hash is what CivetWeb validates, hot-reload through the
file watcher); only the credential values removed.

Both credentials still need rotation: redaction does not un-leak history.
That is an operator action on Dankclaw, not something this commit can do.

Refs #40
The gate scanned only `*.md *.yml *.yaml *.json *.toml *.sh *.ts *.js`. A
`postgres.env`, a `.container` quadlet, an `.ini`, or an extension-less dotfile
— exactly the shapes the dotfiles tier is made of — was never read at all.
Discovered while deciding what else belongs in `dotfiles/`: `~/.config`
carries `containers/systemd/postgres.env` with a real `POSTGRES_PASSWORD`,
which the ignore list did not cover either (`git check-ignore` says so: `.env`
does not match `postgres.env`).

Token SHAPES now scan the entire staged diff, every file type; bcrypt hashes
and `Authorization: Basic` headers joined the shape list. Credential
ASSIGNMENTS are scanned in non-prose files only — the auto-learned memory tier
is prose *about* credentials (7 such hits measured on the tracked tree), and a
gate that fires there gets overridden until it means nothing. Markdown keeps
shape detection, which is what it had before.

Three invariants, each pinned by a test in tests/secret-gate.bats (13 tests,
black-box: the hook is copied into a scratch repo and git itself runs it):

- Fail closed. The first version of this rewrite passed `-I` to `git diff`,
  which is not valid there; the empty file list made every secret commit sail
  through, and a blind gate looks exactly like a clean tree. If the hook cannot
  read the staged diff it now refuses the commit.
- Only ADDED lines can leak. The gate refused the redaction commit in this
  branch because the diff still carried the removed password — a gate that
  blocks the removal of a secret holds the leak hostage.
- Path-valued variables (`…_FILE=`, `…_PATH=`) and placeholder/indirected
  values are not secrets, so the gate stays worth listening to.

`.gitignore` quarantines `*.env` at any name, and gains the theme-media rule
whose untracking lands in the next commit. `AGENTS.md` states the new contract.

Refs #40
Three things were in history that should not have been, all invisible to the
tool that deploys them:

- `home/dank/.omp/dotfiles/**` — 3 files re-hosting `niri/cfg/autostart.kdl`,
  `niri/cfg/keybinds.kdl` and `systemd-user/…/swayidle-lock.service` under a
  prefix `scripts/deploy.sh` never reads (it walks `dotfiles/<pkg>/`). The live
  package is deployed and symlinked, so the copy asserted nothing; it only
  misled. Moved to `.AGENTTRASH/20261020-tracked-debt/`.
- `.pre-pull-backup/**` — 2 managed-skill files, a local backup in history,
  which `.gitignore` itself declares must stay on disk only. Same destination.
- `dotfiles/noctalia/.local/share/noctalia/wallpapers/` — 23 images, 80.4 MiB.
  Untracked, files left in place: `~/.local/share/noctalia/wallpapers` symlinks
  into this directory and the theme reads it, so deleting them would have gone
  black. Every filename carries its wallhaven ID, so a fresh box restores the
  set with one `curl` per file plus `deploy.sh`; losing the set is an acceptable
  outcome, 80 MiB per clone forever is not.

New page `omp-docs/tools/dotfiles-deploy.md` owns the deploy contract (the
`--check`/sync/`--adopt` modes, `zz-$(hostname)` override semantics, the dconf
and systemd-user special cases) because nothing documented it, plus two laws:
media the pipeline consumes is content not config, and `systemctl --user
enable` snapshots an absolute path — so enabling from a worktree means the
seven-day reap silently kills the unit while `is-enabled` keeps answering
`enabled`. That second law is not theoretical:
`default.target.wants/blankpad-desk-autoswitch.service` pointed into the reaped
`wt/de-redesign-20261009` and desk auto-switching had been off since. It is
relinked to `dotfiles/zz-blankpad/` and left inactive rather than started mid-
session; starting it applies a desk profile.

Refs #40
`inventory/home-config-census.md` was dated 2026-08-28, tagged `unverified`,
and every path in it was `/home/dank` — it described another box, yet
`inventory/index.md` still advertised it as the input for the pruning session,
so the pruning session would have pruned against fiction.

Rebuilt from this box, purpose stated in line one. Classification is per FILE
because the tracked unit is a symlink into `dotfiles/`: a directory can look
"real" while every file inside it is tracked (`niri` 12/1, `gtk-3.0` 3/0), and
a directory can be one whole symlink (`lazyvim`). The classifier command is in
the page so tomorrow's agent reruns it instead of trusting the table.

The generated/authored split is the load-bearing part: 24 `noctalia*` palette
files across 20 apps are one generator pass, proven by the
`2026-10-14 19:05:40.330…40.813` same-second cluster and later single re-stamps
(`bat` 10-18, `niri` 10-18, `mango` 10-17). A same-second mass write is
generation, never authorship — so the source palette stays tracked and the
expansion never is. Adoption candidates are limited to programs that pass
`command -v`, with authorship read from the file; the prune list extends the
expected 14 with `pywalfox`, `qt5ct`, `qt6ct`, `turborepo`, and notes that six
of the orphan dirs contain nothing but generated palette.

`fib-watchdog.{service,timer}` is recorded as owned by the bench repo, not
adopted here: its `ExecStart` and `WorkingDirectory` are that project's
internals. It is flagged as a reinstall risk instead, which is the honest
status. Deleted: the `/home/dank` rows, `~/.pi`, Steam library sizes, and the
old offender/summary tables — numbers for software that is not installed here
fail the purpose test.

Refs #40
dank added 1 commit 2026-10-20 17:00:29 -07:00
The repo still tracked `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded`
— a retired output config committed 2026-10-09, five days before `.gitignore` grew the
`.AGENTTRASH/` rule. Tracked-then-ignored: an ignore rule never reaches an already-tracked
path, so `git rm --cached` is the only repair. The working copy joins the rest of today's
tracked debt.

Found while answering "does this repo still track my noctalia and niri settings", which
also falsified two rows of the census committed yesterday: niri is 9 tracked files, not
12; noctalia is 3 authored files against 2672 files of app state and downloaded catalog,
not 2/0. The page's own classifier command reproduces the new numbers — it never
reproduced the old ones, which is the only reason they were wrong and not merely
different.

Two laws recorded alongside, both from the same probe: `config.kdl`'s
`include "noctalia.kdl"` is not `optional=true`, so a fresh box cannot start niri until
the theme pipeline has run once; and `cfg/display.kdl -> desk-1-home.kdl` is desk state a
fresh box does not receive — by design, but nowhere written down.

Refs #40
dank merged commit 689ada428f into main 2026-10-20 17:02:53 -07:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/command-center#41