CC secrets build: register (177 -> 181: R-1130..R-1134 opened, R-1122 closed, R-1118 narrowed), STATUS D13-D16, report
gates / gates (push) Successful in 6m52s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-17 13:42:14 +02:00
parent ba40c0a25b
commit 30b231f447
4 changed files with 138 additions and 5 deletions
+6
View File
@@ -26,6 +26,12 @@
---
## 2026-10-17 — the CC secret broker `sec` and its guards (`admin/cc-secrets`)
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1122** | **`skills/felhom-build-deploy/SKILL.md` told CC to `source ~/.config/credentials` for the ISO publish.** (P3) | CLOSED 2026-10-17 — the ISO publish and the hub-token recipe go through `sec run`; the values reach docker as `-e NAME` and curl through a header file, never on a command line. Both recipes proven: a local server saw the token's hash and length; a read-only `lsf R2:felhom-iso` listed the bucket. | `ba40c0a2`; `audits/cc-creds-build-2026-10-17/` |
## 2026-10-17 — Tester 1 on the live catalog, decoy suites in every runner, memory limits, FileBrowser (controller v0.326.0, catalog `bff82f9`)
| Row | What | Closed | Evidence |
+8 -4
View File
@@ -212,7 +212,7 @@ stopping line that lies.
| **R-1094** | Storage & devices | P4 | **The whole-disk format refusal tells the household to format the free part — which is refused while the other part is in use.** SEEN 2026-10-16 (controller 0.318.0): 409 „Ezen a lemezen egy használatban lévő meghajtó van, ezért a teljes lemez nem formázható. Csak a szabad részét formázhatod." — since agent 0.158.1 a part next to a mounted part can only be USED (the root wrapper trusts no „Felhom's own" claim). Evidence: `audits/storage-identity-buildB-2026-10-15/live/14-samedisk-refusal-offer.txt`. | READY | — | Reword `err.web.meghajto_hasznalatban_formazas` (hu + en): „… nem formázható. A szabad részét használatba veheted." Next controller release; the copy gates run. | CC |
| **R-1124** | Storage & devices | P4 | **A drive decommissioned and then unplugged keeps its agent mount unit enabled on the host** (`mnt-<name>.mount`, „Managed by felhom-agent”, `WantedBy=multi-user.target`), so every later boot waits for a device that is gone, and the agent's watchdog reports it „disconnected”. Decommission retires the drive logically (agent: „logical retire, data untouched”) and releases only the guest bind; no product path releases the host mount or forgets the drive. Seen on Tester 1 at the R-1022 teardown 2026-10-17 (four units left; the boot delay is read from the unit, NOT measured). | OPEN — filed 2026-10-17 | — | Decide „forget a decommissioned drive” (unit disabled + removed when the drive is absent), with a test | CC |
## Security & access — 22 rows (P1 1, P2 2, P3 13, P4 6)
## Security & access — 27 rows (P1 1, P2 2, P3 18, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -233,10 +233,15 @@ stopping line that lies.
| **R-1040** | Security & access | P4 | **Until the household makes Sonarr's or Radarr's first admin, any container on the box can read the app's API key.** Measured on the bench 2026-10-14 with the R-1030 seed in place: `AuthenticationRequired=Enabled`, `AuthenticationMethod=None` — `/initialize.json` from another container answers 200 with `apiKey`. Outsiders are blocked by the box's setup gate in that window; a compromised other app is not. | **OPEN** — filed 2026-10-14 | — | Seed the API key at install (`SONARR__AUTH__APIKEY`, measured to work) so the box knows it, and close the window by telling the household in the first steps; or seed a Forms user | CC |
| **R-1050** | Security & access | P4 | **Raise HSTS to one year on or after 2026-11-15, if no https trouble was reported.** `09` §3 decision 249 (D2 = A, operator 2026-10-15): HSTS stays `max-age=86400` now; after a month with no https trouble it becomes one year (`31536000`); never preload, never `includeSubDomains`. The value is `HSTSMaxAgeSeconds` in `felhom-controller/controller/internal/infra/infra.go`, pinned by `TestRenderForwardedHeaders_HTTPSOnly`. „Trouble” = any report of a name that will not open, or a box answering http after the floor. | **WATCHING** — filed 2026-10-15 | the date | On or after 2026-11-15: read the register and the operator's mail for https trouble since 2026-10-15; none → a controller release with one year (test updated), bake → vouch → floor | CC |
| **R-1054** | Security & access | P3 | **The hub's operator/global API key is still one bearer that can do everything on `/api/v1`, including reading every box's break-glass root password (`GET /admin/hosts/{id}/recovery-credential`).** Hub v0.152.0 gave the web surface scoped tokens (`05` §17) but tokens are refused on `/api/v1` by design. Who uses the global key today is not listed in one place. Fix: list its consumers; move each operator use to a scoped token on the web side (or a token class on the API), and keep the global key only where a box path needs it — or retire it. | OPEN — filed 2026-10-15 by the token build | — | List the global key's consumers (pod env, scripts, runbooks) | CC |
| **R-1118** | Security & access | P3 | **No architecture document covers how CC obtains and uses secrets; four transcript leaks and one wiped credentials file came from that gap** (R-132, R-453, R-1088, `audits/CAMPAIGN-11-recovery-journey-2026-08-05.md` §7). `12-agent-tooling.md` covers instruction files only, `runbooks/secrets.md` cluster Secrets only. The proposed design (encrypted store + `sec` broker + CC guards + nightly sweep) was spiked 2026-10-17 with fake secrets (`audits/SPIKE-cc-creds-2026-10-17.md`): output mask PASS, PreToolUse refuse PASS, PostToolUse replace PARTIAL (a crashing hook and a large output each put the value on disk; both have a measured fix), SOPS + age store PARTIAL (works locally; the private repo could not be created: no token with `write:user`, push-to-create off), transcript sweep PASS. The design file `DESIGN-cc-secrets-2026-10-17.md` was not on DooPlex, so it is not in git yet. | WAITING-ON-OPERATOR | the operator's store choice (B, SOPS + age in a private Gitea repo, recommended; or Vaultwarden) and the design file | Operator: pick the store; put the design file at `audits/SPIKE-cc-creds-2026-10-17/DESIGN.md`; for B, create the empty private repo `admin/cc-secrets-spike` (or give CC a token with `write:user`). Then a build TASK: the architecture document first, then broker and guards with the spike's two fixes | operator → CC |
| **R-1118** | Security & access | P3 | **CC's secrets moved off the plain `~/.config/credentials` into an encrypted store with a broker and guards; what is left is retiring the old file.** BUILT 2026-10-17 (store B, operator ruling 11:28): private repo `admin/cc-secrets` (`ARCHITECTURE.md`, the design verbatim in `docs/`), `sec` (no get), PreToolUse refusals + PostToolUse masking for Bash and Read in `~/.claude/settings.json` (every CC session on DooPlex), nightly transcript sweep with mail. 55 tests, 18 red-proofs, 36-row child-session proof in both launch modes, fresh-session proof (`audits/cc-creds-build-2026-10-17/`). 25 names in the store (23 imported, `RESEND_API` from CC's environment, a self-test value). `12-agent-tooling.md` §5 points at it. | NARROWED — built; the old file's retirement is R-1134 | the week with no reader (R-1134) | `sec doctor` stays all OK; R-1134 | CC |
| **R-1119** | Security & access | P3 | **`RESEND_API` is exported into CC's own environment from `~/.bashrc` (line 88), so every Bash call CC makes carries it, and a plain `printenv`, `env` or `set` prints it into the transcript.** MEASURED 2026-10-17 (SPIKE Q6, names only): present in the parent `claude` process environment and in the Bash tool environment. | OPEN | R-1118's broker, for the clean fix (the value lives in the store and reaches only the command that names it) | Until then: find what reads it (memory: the Resend checks run it via `bash -ic`), then move the export out of `~/.bashrc` into that one script — the operator's shell file, so the operator's word | operator |
| **R-1120** | Security & access | P3 | **`homelab-manifests/.claude/settings.json` allows `Bash(kubectl get:*)` without a prompt, and that pattern includes `kubectl get secret -o yaml`, which prints every value (base64) into the transcript.** Read 2026-10-17 at `origin/main` `e8e595a`, line 6; nothing in the file asks for Secret reads to skip the prompt. The local clone `~/git/homelab-manifests` is behind origin and has uncommitted edits to that same file (not touched). | OPEN | — | Add `Bash(kubectl get secret:*)` and `Bash(kubectl get secrets:*)` to `ask` (ask wins over allow), or a PreToolUse refusal like the spike's `kubectl-read-secret` rule. A permission change, so the operator's word (`.claude/rules/unprompted-work.md` §5) | operator |
| **R-1121** | Security & access | P3 | **`~/rotated-secrets-2026-10-09.txt` still exists on DooPlex** — 632 bytes, modified 2026-10-09 13:08 +0200, mode 600 (SPIKE Q6: `test -e` + `stat` only, not opened). Left by the R-925 rotation work. If any value in it is still valid, it is one more plaintext secrets file in the home folder, readable by every CC session. | WAITING-ON-OPERATOR | what the file holds — only the operator knows | Operator: confirm every value in it is revoked, then `shred -u` it; or move the live ones into the R-1118 store | operator |
| **R-1130** | Security & access | P3 | **`DOCKER_PASSWORD` (the Gitea registry login CC uses) is in one old CC session record** — a helper-agent transcript of 2026-09-21 (`~/.claude/projects/-mnt-5-hdd-felhom-eu-git/5f83c0fb…/subagents/agent-a8614eb6ee2a17e6c.jsonl`). Found by the first history sweep 2026-10-17 (`audits/cc-creds-build-2026-10-17/history-sweep-list.txt`; names only). The record has reached Anthropic's API; the value is still valid unless changed. | WAITING-ON-OPERATOR | the operator rotates it, or tells CC to (file → store with `sec put`) | Rotate the registry password; `sec put DOCKER_PASSWORD --from-file F`; check an image pull | operator |
| **R-1131** | Security & access | P3 | **demo-felhom's recovery code (`R_DEMO_FELHOM`) was printed into the JARR session's record today, 2026-10-17 08:27 UTC**, by `set -a; . ~/.config/credentials` — the Campaign 11 §7 shape (a hyphenated key bash cannot assign, so bash prints the line). This was 3 h before the guards were installed; that command is refused now (`source-secret-file`). Record: `~/.claude/projects/-home-kisfenyo-git-jarr/c9d98cda….jsonl` line 2855 (a tool result). | WAITING-ON-OPERATOR | the operator: re-issue the code, or accept (a demo box) | Decide: re-issue demo-felhom's recovery code, or accept and close | operator |
| **R-1132** | Security & access | P3 | **demo-hp's recovery code (`R_DEMO_HP`) was printed in the same JARR record line as R-1131** (2026-10-17 08:27 UTC, the same `. ~/.config/credentials`). | WAITING-ON-OPERATOR | the operator: re-issue, or accept (a demo box) | Decide together with R-1131 | operator |
| **R-1133** | Security & access | P3 | **`~/.git-credentials` on DooPlex holds Gitea tokens in clear text** — three entries (`kisfenyo`, `Kisfenyo`, `admin`); the `admin` token has `write:repository` (and misc, package, issue, notification). Any `cat` of it prints them; the new PreToolUse guard refuses reads of that path (`read-secret-file`), but git itself must keep reading it. Out of the 2026-10-17 build's scope by its brief. | OPEN | — | Decide: move git to a credential helper that reads from `sec`, or accept the file (0600) with the guard | operator → CC |
| **R-1134** | Security & access | P3 | **Retire the old `~/.config/credentials`**: every named key is in the CC store since 2026-10-17 13:16 (23 imported; the two hyphenated names became `R_DEMO_FELHOM`, `R_DEMO_HP`). **Two open points:** (1) its line 20 is a 32-character value with NO name — the import refused it (one bad line refuses all) and CC imported the named lines from a filtered copy; only the operator knows what it is. (2) `FACEBOOK_API` and the Tuya keys, which `fb_probe.py` and the bench README name, are in neither the file nor the store. | WAITING-ON-OPERATOR | a week with no reader (from 2026-10-17), and the operator's go | 2026-10-24: check no reader (sweep + `hook.log`); operator names line 20 (or drops it); then rename the file to `.retired`, and delete only on the operator's go | operator → CC |
| **R-1126** | Security & access | P3 | **The Cloudflare tunnel runs before the box is claimed, and no document says whether that is intended.** The claim needs it: the claim mail links `https://felhom.<domain>` (`hub/internal/notify/templates.go:27`); `cloudflared` is started from the token with no claim check (`controller` `stacks/infra.go:85-90`; `01-topology-and-trust.md:119`, `:248`). Reachable before the claim: the dashboard serves only the claim page (code: 72 h life, 5 tries, 15 min lockout — `web/claim.go:22-33`), and `files.<domain>` — FileBrowser on its default login from its first start until the next base-stack tick sets a password (seconds; v0.326.0 re-checks every run). Options: (A) keep the tunnel; route FileBrowser publicly only after its default is replaced — small, no promise changes; (B) start the tunnel only after a LAN claim — the claim flow and its mail change, the household must be at home. Recommended A. From R-977. | WAITING-ON-OPERATOR — filed 2026-10-17 | the operator's choice A or B | Write the choice into `01` §5; build A (or B) | CC |
## Box system & updates — 12 rows (P2 1, P3 7, P4 4)
@@ -311,7 +316,7 @@ stopping line that lies.
| **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator |
| **R-1035** | Business & legal | P4 | **Did the „felhő tárhely” page earn anything? Two dated checks.** **Baseline (reported by the operator from Search Console, 2026-10-14, last 3 months):** 1 click, 48 impressions, CTR 2.1 %, average position **73.6**; „felhő tárhely” 19 impressions, „felhőtárhely” 5, „felhő alapú tárhely” 3. CC measured separately that the phrase „felhő tárhely” appeared **0 times** on the home, FAQ and backup pages — Google was showing the home page for those queries by accident. `/sajat-felho-tarhely` + `/en/own-cloud-storage` published **2026-10-14**; full baseline, price sources and the power provenance in `audits/seo-2026-10-14/BASELINE.md`. **What to measure, both times:** impressions and average position for „felhő tárhely” and „saját felhő tárhely”, plus the new page's own impressions, clicks and position. **A null result at +4 weeks means nothing on its own** — a new page on a site at position 73.6 takes weeks to months to move, and „felhő tárhely” alone is held by Microsoft, Google and the big list sites; the page aims at the two longer phrases. That is why there are two checks and not one, and why neither should be read as a verdict on the copy. **There is no Search Console connector in this project, so CC cannot read these numbers** — the operator reads them, CC records and compares. **Blocked until the operator asks for indexing** (Search Console → URL-ellenőrzés → the URL → „Indexelés kérése”): until that happens, or Google finds the page itself, both checks read zero for a reason that is not the page. **This is a slow channel and does not replace talking to people.** | **WATCHING** | operator: request indexing, then supply the Search Console read | 2026-11-11: first read (+4 weeks); 2026-12-09: second (+8 weeks), compared with the first | operator + CC |
## Process & tooling — 32 rows (P2 1, P3 10, P4 21)
## Process & tooling — 31 rows (P2 1, P3 9, P4 21)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -327,7 +332,6 @@ stopping line that lies.
| **R-902** | Process & tooling | P3 | **The contact form's mailer has no source in any repository: the running image cannot be rebuilt or changed.** FOUND 2026-10-08 (website refresh, brief Part C5). `manifests/contact-mailer.yaml` names `contact-mailer:latest` with `imagePullPolicy: Never` (imported into k3s by hand); the binary in the pod is `/app/contact-mailer` dated 2026-02-05, Go module `github.com/felhom/contact-mailer` (devel). Searched: `git log --all` of felhom.eu (only the manifest), every repo under `/mnt/5_hdd/felhom.eu/git`, `find /mnt/5_hdd/felhom.eu ~ -maxdepth 5 -iname '*contact-mailer*'`. READ from the binary (strings, read-only `kubectl exec … cat`): ONE send per form (`sendViaResend`) to `TO_EMAIL`, `reply_to` = the visitor; nothing is sent back to the visitor; the operator's notification is Hungarian (`buildEmailHTML`, „Csatolmány … fájl"). The English form needs no mailer change (its subject labels carry „(EN)"). `audits/website-refresh-2026-10-08/mailer.md` | **OPEN — NARROWED 2026-10-08 (search done, NOT FOUND): owner: operator.** A read-only search found no source on DooPlex (55,548 `*.go`/`go.mod`/`Dockerfile*` files, every disk, media/Longhorn/containerd/PBS excluded by name), in the web editor's storage and file history, in all 10 Gitea repositories' history, or in Docker's build cache (oldest record 2026-09-28). The binary's own clues: one file `/build/main.go`, go1.23.12, no `vcs.*` stamp; image built 2026-02-05 10:40 by BuildKit. The binary is now KEPT in git (`audits/mailer-source-2026-10-08/contact-mailer.bin`), so a DooPlex rebuild no longer loses the program. A replacement plan is written (`audits/mailer-source-2026-10-08/PLAN-replacement.md`); one question left: is the February folder on the operator's Windows workstation? | — | Operator: look on the workstation; if absent, build the replacement from the plan (attended deploy) | operator |
| **R-934** | Process & tooling | P3 | **Pull the failed disk ST3000VX010 S/N Z6A07P2G out of DooPlex; follow the checklist.** The data was moved off at the first alarm (2026-10-10); the disk is unmounted, its fstab line commented out since 2026-08-14, k3s no longer waits for it, and its empty Longhorn disk entry was removed 2026-10-12. Its two SMART alarms (352 unreadable / offline-uncorrectable sectors, flat since 2026-10-09) are silenced by serial until **2026-11-11 14:32 UTC** — if the disk is still in the machine then, they come back on purpose. **A TWIN of the same model (S/N Z6A070XZ, `/mnt/3_hdd`) is in use: read the serial before unplugging.** Checklist: `runbooks/dooplex-remove-disk-Z6A07P2G.md`. | **OPEN** — NEW 2026-10-12 | the operator (hands, a power-off window) | Follow the checklist; after the first boot remove the exporter line and the silence; tell CC | operator |
| **R-935** | Process & tooling | P3 | **Something sets `core.bare = true` on the shared `felhom.eu` checkout, and then every git command there fails („this operation must be run in a work tree").** SEEN 2026-10-12 evening twice: `.git/config` changed at 16:29:37 and 16:37:33 CEST, each time with `core.bare=true`; restored by hand both times (`git config core.bare false`, a flag only, no file touched). **Not caused by the gates:** `repo_gates.py --fast` run plainly and with `GIT_DIR=.git` (as a hook gets it) left the flag `false`; a scratch `git init --bare <path>` under `GIT_DIR` does not flip the parent either. The first flip came while this session did not touch the repo; another session pushed to main at 16:44 from elsewhere. | **OPEN** — 2026-10-12 later: across this session's next push the flag stayed `false` (read before and after); not this session's pushes. **OPEN** — NEW 2026-10-12 **Note 2026-10-12 (security review):** at least some of these flips match the security review's pushes from a git worktree of this clone (about 16:29 and 16:37): inside the pre-push hook, `scripts/test_gate_decoys.py`'s poster-facts case runs `git init`/`git config`/`git commit` with the hook's inherited `GIT_DIR`, so it wrote into the real repository — four test commits in the worktree and `user.name d` / `user.email d@d` in the shared `.git/config` (the review removed that `[user]` section at about 16:50; commits 6c2c3dd5 and 186f3aa3 carry that author). The review stopped pushing from the worktree. Fix: the test helper must drop `GIT_DIR`, `GIT_WORK_TREE` and `GIT_INDEX_FILE` from its environment. | finding the writer | Watch `.git/config` (an inotify or audit rule on the file) during a day with the parallel sessions; name the command; fix it there | CC |
| **R-1122** | Process & tooling | P3 | **`skills/felhom-build-deploy/SKILL.md` line 153 still tells CC to `source ~/.config/credentials` for the ISO publish** — the shape that printed two recovery codes into a transcript (Campaign 11 §7; memory `credentials-file-sourcing-leaks-codes`), while line 75 of the same skill says never to `source` that file. Found 2026-10-17 by the CC-secrets spike, which stayed docs-only by its brief. | OPEN | — | Replace with three `scripts/read_credential.py` reads into 0600 files (or `sec run` once R-1118 is built) | CC |
| **R-206** | Process & tooling | P4 | **The build-cache cap and the weekly prune exist only as a hand-edited `/etc/docker/daemon.json` on DooPlex — not in Ansible, so a rebuild loses them.** The `node_housekeeping` role must also carry the prune, which today it is forbidden to run | **NARROWED** — **2026-10-12 evening: part (a) is in git** (homelab-manifests `eba52d7`, role `docker_daemon`, tags `never` + `docker_daemon`: a byte-copy of the live `daemon.json` and a restart-and-verify handler). **Not installed** — installing restarts the Docker engine on DooPlex (the operator's window). Part (b), narrowing the housekeeping ban to `docker builder prune -af`, loosens a safety rule: the operator's word. Part (c) is in the role's handler. **READY (M) — NEW 2026-08-05** | — | Operator: install in a window (`ansible-playbook site.yml --tags docker_daemon`, check first) — and say yes/no to (b) | operator |
| **R-928** | Process & tooling | P4 | **Two sessions running `repo_gates.py` at the same time in the shared `felhom.eu` clone see each other's DECOY plants.** SEEN 2026-10-10 ~15:40: while another session's gate run was in its decoy suite, this session's working tree showed `website/assets/crafty-controller-logo.png` renamed to `crafty-logo.png`, `website/en/apps.html` with a leftover text language switch, then `website/en/contact.html` and `scripts/felhom-host-install.sh` modified — each a plant of `test_gate_decoys.py`, each gone when that run ended. This session's own `site_gates.py` failed three times in that window on plants, not on its change, and a `git add` of a broad path at that moment would have committed a plant. A killed run (`timeout`) leaves its plant in place (its cleanup never runs). Fix direction (a design): decoys plant into a temporary COPY of the tree (as the catalog's volume-persistence decoys already do), or a lock file that a second run waits on. | **OPEN** — filed 2026-10-10 (register-shrink session) | — | Design the copy-or-lock; meanwhile stage explicit paths only (already the rule) and re-run a red site gate once another session's run has ended | CC |
| **R-394** | Process & tooling | P4 | **`felhom-build-deploy/SKILL.md` is 179 lines, over the 150-line limit its own repo now enforces.** Found 2026-08-25 by `scripts/check_skills.py` on its first run — the over-length was discovered BY the new checker, on the day the limit was written down, which is the checker working as intended. **It is not edited and not trimmed here:** the task that introduced the limit explicitly scoped the four pre-existing skills out, and trimming a build-and-deploy skill without exercising its commands is how a wrong command ships to a live host. **It is a named single-entry exception in `GRANDFATHERED` in `scripts/check_skills.py`, printed as a WARN on every run**, so it cannot fade; a NEW skill over the limit is convicted normally, and growing the set requires editing that file in a commit with a row to name. **The rationale for the limit** — attention thins across the excess, so the lines that matter are not the ones that survive — is in `skills/felhom-doc-authoring/SKILL.md` §5. | **OPEN — LOW** | — | Trim `felhom-build-deploy/SKILL.md` under 150 lines in a session that can VERIFY the commands it keeps, then delete its `GRANDFATHERED` entry in the same commit. The likely trim is the per-artifact command blocks moving behind a pointer to the runbooks, keeping the gotchas inline — but that is a judgement for a session with a build to run, not a line-count exercise. | CC |