# -*- coding: utf-8 -*- """Instruction-file consistency gate — keeps CLAUDE.md files from silently regrowing. Usage: python3 scripts/instructions_gate.py [ ...] python3 scripts/instructions_gate.py --fast (identical: no network, no container runtime) THIS IS A CONSISTENCY GATE, NOT A BUDGET GATE. Measured 2026-08-06 on a 1M-token window: the instruction files occupied 15k tokens against 869k free. Space is NOT the constraint here and no failure message may claim it is. The line ceiling exists because longer instruction files reduce ADHERENCE — Anthropic's guidance is to keep a CLAUDE.md under 200 lines — and because a file nobody can hold in their head is where contradictions hide. Four were found in this project on 2026-08-06, two of which decided where a destructive drill runs. WHAT IS COUNTED. Every check runs against EFFECTIVE text: block-level HTML comments are stripped first, because they are stripped before injection and never reach the model. Verified empirically on Claude Code 2.1.222 with a control (two plain markers -> both seen) and a treatment (one marker inside , twice -> not seen). That is the whole point of the comment convention: earned rationale stays in the repo for human readers at zero cost to the instructions. CHECKS (each names the file and the reason; a missing input is a FAILURE, never a skip): 1. /CLAUDE.md is at most MAX_LINES effective lines. 2. Every /.claude/rules/*.md declares `paths:` in frontmatter, or `unconditional: true`. NOTE: `paths:` is a Claude Code feature — a rule carrying it loads only when a file matching one of its globs is read. `unconditional: true` is NOT a product feature; it is OUR marker, asserting that always-loading was deliberate. The product loads a rule with no `paths:` key unconditionally either way, so this check catches the ACCIDENT, not the product behaviour. 3. No component version literal in effective text. Versions change several times a day and the fleet is not uniform, so a version in an instruction file is stale within a day (ask the hub's /hosts + /configs, or the box). Historical citations belong in an HTML comment beside the rule they justify, where they inform a human and cannot go stale in the model's view. IPv4 addresses are NOT versions — a bare \\d+\\.\\d+\\.\\d+ matches the first three octets of every one of them, which is how this check would otherwise fail on its own repo. 4. No TEMPORARY block carrying a date already past. The block this gate was written for said "TEMPORARY - until ~2026-08-02 ... Delete this block on return" and was still being read as current fact on 2026-08-06, while a sibling repo's CLAUDE.md asserted the opposite. 5. The workspace-root CLAUDE.md and its versioned copy at felhom.eu/documentation/runbooks/workspace-CLAUDE.md are byte-identical. The live file sits in a directory that is not a git repo, so the copy is the only version-controlled record of it; nothing but this check enforces that they agree. THE POSITIVE OBSERVABLE. Every root prints a per-check tally with the measured numbers, not just a verdict. "0 failures" alone cannot tell a working gate from a blind one — if the effective line count suddenly reads 3, the stripper broke, and the count is where you see it. The kill condition is pinned by scripts/test_instructions_gate.py: an over-length CLAUDE.md still FAILS, and a rule file with neither marker still FAILS. """ import os import re import sys MAX_LINES = 200 # Block-level HTML comments: stripped before injection, so they cost nothing and are not counted. COMMENT_RE = re.compile(r"", re.S) # A semver-ish literal that is NOT part of a dotted quad (IPv4) and not part of a longer run. VERSION_RE = re.compile(r"(? MAX_LINES: failures.append( "%s: %d effective lines, ceiling %d. This is an ADHERENCE limit, not a space " "limit — long instruction files get followed less reliably and hide " "contradictions. Move path-bound guidance into .claude/rules/*.md with a `paths:` " "list, procedures into the skill that already covers them, and earned rationale " "into an HTML comment (free: stripped before injection)." % (path, n, MAX_LINES) ) def check_rules(root, failures, tally): rules_dir = os.path.join(root, ".claude", "rules") if not os.path.isdir(rules_dir): tally.append(" rule files : none (no .claude/rules/)") return names = sorted(n for n in os.listdir(rules_dir) if n.endswith(".md")) scoped = 0 for name in names: path = os.path.join(rules_dir, name) with io_open(path) as fh: head = fh.read(2048) has_paths = re.search(r"^paths:", head, re.M) is not None has_uncond = re.search(r"^unconditional:\s*true\s*$", head, re.M) is not None if has_paths: scoped += 1 elif not has_uncond: failures.append( "%s: rule file has neither a `paths:` frontmatter list nor an explicit " "`unconditional: true`. Without `paths:` Claude Code loads it in EVERY session, " "which is rarely what a rule file is for — add the globs it applies to, or " "declare `unconditional: true` to say the always-loading is deliberate." % path ) tally.append( " rule files : %d (%d path-scoped)" % (len(names), scoped) ) def check_versions(path, failures, tally): with io_open(path) as fh: eff = effective(fh.read()) hits = [] for i, line in enumerate(eff.split("\n"), 1): for m in VERSION_RE.finditer(line): hits.append((i, m.group(), line.strip()[:90])) tally.append(" version literals : %d" % len(hits)) for lineno, ver, ctx in hits: failures.append( "%s:%d: component version literal %r — versions change several times a day and the " "fleet is not uniform, so this is stale within a day. Ask the hub (/hosts, /configs) " "or the box. A historical citation belongs in an HTML comment beside the rule it " "justifies.\n %s" % (path, lineno, ver, ctx) ) def check_temporary(path, failures, tally): with io_open(path) as fh: eff = effective(fh.read()) lines = eff.split("\n") today = today_tuple() found = 0 for i, line in enumerate(lines, 1): if not TEMPORARY_RE.search(line): continue found += 1 window = "\n".join(lines[i - 1 : i + 6]) for m in ISO_DATE_RE.finditer(window): when = tuple(int(g) for g in m.groups()) if when < today: failures.append( "%s:%d: TEMPORARY block carrying the past date %s. A temporary block that " "outlives its own deadline is read as current fact — this gate exists " "because one did, for four days, while a sibling repo asserted the " "opposite. Delete it; the audit trail is the record." % (path, i, m.group()) ) break tally.append(" TEMPORARY blocks : %d" % found) def check_workspace_copy(workspace_root, failures, tally): live = os.path.join(workspace_root, "CLAUDE.md") copy = os.path.join(workspace_root, WORKSPACE_COPY) if not os.path.exists(live) or not os.path.exists(copy): tally.append(" workspace copy : n/a (not this workspace)") return with io_open(live) as a, io_open(copy) as b: same = a.read() == b.read() tally.append(" workspace copy identical : %s" % ("yes" if same else "NO")) if not same: failures.append( "%s and %s have diverged. The live workspace file sits in a directory that is not a " "git repo, so the copy is its only version-controlled record — nothing but this " "check enforces that they agree. Copy the live file over the versioned one." % (live, copy) ) def io_open(path): return open(path, "r", encoding="utf-8") def main(argv): roots = [a for a in argv[1:] if a != "--fast"] if not roots: sys.stderr.write("usage: instructions_gate.py [--fast] ...\n") return 2 failures = [] for root in roots: root = os.path.abspath(root) print("instructions_gate: %s" % root) tally = [] claude_md = os.path.join(root, "CLAUDE.md") if not os.path.exists(claude_md): failures.append( "%s: no CLAUDE.md. A missing input is a FAILURE, never a skip — a gate that " "silently passes on an absent file is how the check stops running." % root ) else: check_length(claude_md, failures, tally) check_versions(claude_md, failures, tally) check_temporary(claude_md, failures, tally) check_rules(root, failures, tally) check_workspace_copy(os.path.dirname(root), failures, tally) for line in tally: print(line) if failures: print("") print("instructions_gate: %d FAILURE(S)" % len(failures)) for f in failures: print(" - %s" % f) return 1 print("") print("instructions_gate: OK") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))