diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-02 05:19:01 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-02 05:19:01 -0400 |
| commit | b6a977cec25fddf1e498896cec3ad9462fc149db (patch) | |
| tree | 9bbd5a1ac2609c9d1b4cba719360fa7e319db85f | |
| parent | 78bbaae5d8634d52588c1a60d7b7f430bed238c7 (diff) | |
| download | rulesets-b6a977cec25fddf1e498896cec3ad9462fc149db.tar.gz rulesets-b6a977cec25fddf1e498896cec3ad9462fc149db.zip | |
feat(rules): add the host-identity guard rule and startup probe
A tracked or synced doc asserting "this machine is X" is false on every machine but its origin, and an agent trusting it reasons backwards all session. It happened live: a stale "ratio" claim steered a session running on velox. The new rule bans fixed identity claims in tracked/synced docs and requires the runtime derivation instead (uname -n, since the hostname binary is often absent). Describing the fleet stays legal. Claiming the current member doesn't.
startup gained a read-only probe that greps CLAUDE.md and notes.org for the pattern and surfaces hits as a judgment flag, never a block. Fixture-verified under bash and zsh.
| -rw-r--r-- | .ai/workflows/startup.org | 10 | ||||
| -rw-r--r-- | claude-rules/host-identity.md | 20 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/startup.org | 10 | ||||
| -rw-r--r-- | todo.org | 5 |
4 files changed, 44 insertions, 1 deletions
diff --git a/.ai/workflows/startup.org b/.ai/workflows/startup.org index 9488dd0..943bbea 100644 --- a/.ai/workflows/startup.org +++ b/.ai/workflows/startup.org @@ -176,6 +176,15 @@ These calls have no dependencies on each other. Issue them all together in one m The stray-root check uses =find= rather than a glob so the probe behaves identically under bash and zsh (=compgen= is bash-only, and zsh aborts on an unmatched glob). +13. Host-identity probe (see the host-identity rule in =claude-rules/=). Read-only; flags fixed machine-identity claims in the project's tracked/synced docs — the "This machine is ratio" trap, false on every machine but the one that wrote it. Silent when nothing matches. + + #+begin_src bash + grep -inE '\b(this|the current) (machine|host|box|laptop|workstation) is ' \ + CLAUDE.md .ai/notes.org 2>/dev/null | head -3 || true + #+end_src + + Fleet descriptions ("the fleet is ratio and velox") and runtime derivations ("run =uname -n= to find the hostname") don't match — only current-identity assertions do. Fixture-verified under bash and zsh. + Notes on the rsync commands: - Trailing slashes on both source and destination matter — they tell rsync to sync /contents/ rather than nest a directory inside. - =--delete= on the directory syncs lets retired template files actually disappear from each project on next startup. @@ -210,6 +219,7 @@ This phase touches the user and runs sequentially: - *KB consult nudge (read side).* If the Phase A KB-surface prep returned any =kb-relevant-titles=, surface one line listing them (capped 5): "KB lessons that may be relevant: =<title>=; =<title>=… — open the node before related work." The titles are declarative, so the list alone tells you whether to open one. Gated on the roam clone; silent when the clone is absent or nothing relevant surfaced. See the best-practices node and =knowledge-base.md=. - *KB contribute nudge (write side).* Once per session, surface one line pointing at the best-practices node (the =kb-bestpractices= path from Phase A): "Learned something durable? See =<path>= for how to write a KB node — contributing cross-project facts is welcome (personal projects only; work/unknown projects never write per =knowledge-base.md=)." Light encouragement, never a gate. Gated on the roam clone; silent when absent. - *Spec-sort nudge.* If the Phase A spec-sort probe printed =spec-sort: unsorted docs present=, surface one line: "this project's docs pile has never been spec-sorted — say 'run spec-sort' to sort it." If the probe was silent, say nothing. A project with nothing to sort never sees the line; a stamped =:LAST_SPEC_SORT:= marker permanently clears it. See the docs-lifecycle rule and the spec in =docs/specs/=. + - *Host-identity flag.* If the Phase A host-identity probe printed any match, surface it with the file:line and the fix: "this doc asserts a fixed machine identity — false on every other machine; replace with a runtime derivation (run =uname -n=), per the host-identity rule." The probe flags for judgment, never blocks. Silent when the probe is silent. - *Language-bundle sync.* If the Phase A step-12 call (=sync-language-bundle.sh=) printed anything, surface it. =fixed= lines are informational — the drift was already repaired (note that =.claude/= is now dirty if the project commits it). A =drift= line on =settings.json= is surface-only and needs the printed =make install-<lang> PROJECT=.= to reconcile; flag it so the user can decide. If the call was silent, say nothing. - *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, hook, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). For a newly-linked *hook*, note that the harness reads hooks at session start — it fires from the next session (or after Craig opens =/hooks= once); its settings.json wiring travels with the tracked file, so the link is usually the only missing piece. A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing. - *Template-sync churn (safety net).* Check whether Phase A's rsync left uncommitted churn in the synced =.ai/= paths — accumulated from a prior session that crashed before wrap-up, or freshly added this session when rulesets advanced. Without surfacing, it builds up silently until it blocks Phase A.0's auto-ff (git won't ff a dirty tree). Skip in the rulesets repo itself (there =.ai/= is a committed mirror, kept honest by the pre-commit hook). The check is sequential here, after the rsync has finished — not a Phase A step, to keep that batch race-free. diff --git a/claude-rules/host-identity.md b/claude-rules/host-identity.md new file mode 100644 index 0000000..9f58392 --- /dev/null +++ b/claude-rules/host-identity.md @@ -0,0 +1,20 @@ +# Host-Identity Guard + +Applies to: `**/*` (any tracked or synced project file) + +Never assert mutable environment identity as a fixed fact in a file that git tracks or the template sync distributes. A `CLAUDE.md` or notes file claiming "This machine is ratio", a current OS version, an IP, or "the laptop" lands identical on every machine, so the claim is false everywhere but its origin — and an agent that trusts it reasons backwards the whole session. + +## The Rule + +- **Don't write fixed identity claims** — "this machine is X", "the current host is X", "we're on the laptop" — in `CLAUDE.md`, `notes.org`, rules files, or any other tracked/synced doc. +- **Derive identity at runtime and name the command.** The correct phrasing in a doc is an instruction, not a fact: "run `uname -n` to find the hostname." (`uname -n` is the source of truth — the `hostname` binary is often absent, and `uname -r` is the kernel release, not the host.) +- **Describing the fleet is fine; claiming the current member is not.** "The fleet is ratio (workstation) and velox (laptop)" is a durable fact and belongs in a doc (see `daily-drivers.md`). "This machine is ratio" is a snapshot that rots the moment the file syncs. +- The same applies to any mutable environment fact: current OS release, current IP, current display topology. State how to derive it, not what it was when the file was written. + +## Worked failure + +archsetup, 2026-06-21: its `CLAUDE.md` asserted "This machine is **ratio**" as a fixed fact. A session running on velox reasoned from that line all session — skipping velox-only reminders as "not applicable, we're on ratio" — exactly backwards. The fix replaced the claim with the `uname -n` instruction. + +## Enforcement + +The startup workflow runs a read-only probe that greps `CLAUDE.md` and `.ai/notes.org` for fixed-identity phrasing and surfaces any hit as a startup finding. The probe flags for human judgment; it never blocks. When it fires, replace the claim with the runtime derivation, not a fresher snapshot. diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org index 9488dd0..943bbea 100644 --- a/claude-templates/.ai/workflows/startup.org +++ b/claude-templates/.ai/workflows/startup.org @@ -176,6 +176,15 @@ These calls have no dependencies on each other. Issue them all together in one m The stray-root check uses =find= rather than a glob so the probe behaves identically under bash and zsh (=compgen= is bash-only, and zsh aborts on an unmatched glob). +13. Host-identity probe (see the host-identity rule in =claude-rules/=). Read-only; flags fixed machine-identity claims in the project's tracked/synced docs — the "This machine is ratio" trap, false on every machine but the one that wrote it. Silent when nothing matches. + + #+begin_src bash + grep -inE '\b(this|the current) (machine|host|box|laptop|workstation) is ' \ + CLAUDE.md .ai/notes.org 2>/dev/null | head -3 || true + #+end_src + + Fleet descriptions ("the fleet is ratio and velox") and runtime derivations ("run =uname -n= to find the hostname") don't match — only current-identity assertions do. Fixture-verified under bash and zsh. + Notes on the rsync commands: - Trailing slashes on both source and destination matter — they tell rsync to sync /contents/ rather than nest a directory inside. - =--delete= on the directory syncs lets retired template files actually disappear from each project on next startup. @@ -210,6 +219,7 @@ This phase touches the user and runs sequentially: - *KB consult nudge (read side).* If the Phase A KB-surface prep returned any =kb-relevant-titles=, surface one line listing them (capped 5): "KB lessons that may be relevant: =<title>=; =<title>=… — open the node before related work." The titles are declarative, so the list alone tells you whether to open one. Gated on the roam clone; silent when the clone is absent or nothing relevant surfaced. See the best-practices node and =knowledge-base.md=. - *KB contribute nudge (write side).* Once per session, surface one line pointing at the best-practices node (the =kb-bestpractices= path from Phase A): "Learned something durable? See =<path>= for how to write a KB node — contributing cross-project facts is welcome (personal projects only; work/unknown projects never write per =knowledge-base.md=)." Light encouragement, never a gate. Gated on the roam clone; silent when absent. - *Spec-sort nudge.* If the Phase A spec-sort probe printed =spec-sort: unsorted docs present=, surface one line: "this project's docs pile has never been spec-sorted — say 'run spec-sort' to sort it." If the probe was silent, say nothing. A project with nothing to sort never sees the line; a stamped =:LAST_SPEC_SORT:= marker permanently clears it. See the docs-lifecycle rule and the spec in =docs/specs/=. + - *Host-identity flag.* If the Phase A host-identity probe printed any match, surface it with the file:line and the fix: "this doc asserts a fixed machine identity — false on every other machine; replace with a runtime derivation (run =uname -n=), per the host-identity rule." The probe flags for judgment, never blocks. Silent when the probe is silent. - *Language-bundle sync.* If the Phase A step-12 call (=sync-language-bundle.sh=) printed anything, surface it. =fixed= lines are informational — the drift was already repaired (note that =.claude/= is now dirty if the project commits it). A =drift= line on =settings.json= is surface-only and needs the printed =make install-<lang> PROJECT=.= to reconcile; flag it so the user can decide. If the call was silent, say nothing. - *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, hook, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). For a newly-linked *hook*, note that the harness reads hooks at session start — it fires from the next session (or after Craig opens =/hooks= once); its settings.json wiring travels with the tracked file, so the link is usually the only missing piece. A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing. - *Template-sync churn (safety net).* Check whether Phase A's rsync left uncommitted churn in the synced =.ai/= paths — accumulated from a prior session that crashed before wrap-up, or freshly added this session when rulesets advanced. Without surfacing, it builds up silently until it blocks Phase A.0's auto-ff (git won't ff a dirty tree). Skip in the rulesets repo itself (there =.ai/= is a committed mirror, kept honest by the pre-commit hook). The check is sequential here, after the rsync has finished — not a Phase A step, to keep that batch race-free. @@ -161,7 +161,8 @@ The work project edited two synced scripts locally as a stopgap (2026-06-17) and Note (2026-06-24): the Anki =#+TITLE= deck-name fix landed (commit 060a938) — =default_deck_name= is now =default_deck_name(input_path, org_text)= with a new docstring. The preserved 2026-06-17 =to-anki.py= predates that, so *don't* copy it wholesale (it would revert the title-fix). Re-derive the multi-tag changes against the current canonical =flashcard-to-anki.py= and keep the =#+TITLE= behavior. -** TODO [#C] Guard against hardcoded host identity in synced files :feature:solo: +** DONE [#C] Guard against hardcoded host identity in synced files :feature:solo: +CLOSED: [2026-07-02 Thu] :PROPERTIES: :CREATED: [2026-06-22 Mon] :LAST_REVIEWED: 2026-06-24 @@ -170,6 +171,8 @@ A =CLAUDE.md= / notes file that asserts mutable environment identity as a fixed 2026-07-02 Thu @ 05:09:58 -0400 — Craig (speedrun pre-flight): rule + startup lint. A new claude-rules file plus a cheap grep probe in startup flagging host-identity claims in CLAUDE.md / notes.org fleet-wide. +Resolution 2026-07-02: claude-rules/host-identity.md written (fixed-identity claims banned in tracked/synced docs, runtime derivation via uname -n, fleet-description carve-out, the archsetup worked failure) and linked machine-wide by make install. startup.org gained Phase A probe 13 (grep for "this machine/host/box is" claims in CLAUDE.md + notes.org, fixture-verified bash+zsh) and the Phase C host-identity flag line. Flags for judgment, never blocks. + ** TODO [#C] coverage-summary.el documented as a local-only helper :chore: :PROPERTIES: :CREATED: [2026-06-22 Mon] |
