docs(ops-rules): precedence tiers + rules 11-17 - #941
Merged
Conversation
Rule numbers were insertion order, not priority — Rule 5 sat physically before Rule 4 and nothing said which rule wins in a conflict. Renumbering was not an option: ~40 sites across other skills cite these numbers. So the order now comes from a tier, not the number: - Tier 1 Gates (0, 5, 6, 11, 12, 15) — irreversible, outward-facing, money - Tier 2 Truth (3, 8, 9, 13, 14, 16) — what may be claimed, and verified how - Tier 3 Form (1, 2, 4, 7, 10, 17) — output shape, tool limits, ergonomics Gates beat everything and stop the work; Truth beats Form. Brevity, mobile formatting and auto-proceed are explicitly Tier 3, so none of them can authorise a send, a purchase, or a skipped check. Rule 4 moved above Rule 5 to read in order. Numbers unchanged. Six new rules, each from a real incident rather than a guess: - 11 Money is a gate like outbound. Purchases, top-ups, refunds and cap raises had no gate at all: Rule 5 covers infra, Rule 6 covers messages. Match a transaction on date and description, never on amount — identical amounts for purchase and renewal make an amount search ambiguous, and the failure mode is a double refund. - 12 Never change a credential; never recover an account. Using a stored credential stays free. A recovery code you can read is not permission to use it. - 13 Verify at the layer that matters. A handshake is not a working tool, a green deploy is not a landed artifact, a red job is not proof nothing shipped, and a skipped required check reads as passed. - 14 Never fabricate a value to satisfy a check, and never attribute a number to a third party that did not produce it. - 15 Attribution. Sent-from-the-account is not written-by-the-operator, and an echo channel is never an approval. - 16 Claim shared work, and prove another session is not already doing it before adopting its work. - 17 Own the substrate. Config in a vendor-managed tree reverts on update; never pin a model id; low context is not a reason to stop. Rule 9 gains one sharpening: "nobody replied" is not "unread" — the test is who spoke last, not the read flag. Verified: test-skills-lint 791 passed, test-plugin-validate 7 passed, test-no-secrets 27 passed (incl. the operator identity denylist). Added lines scanned for emails, URLs, IPs, home paths and handles: none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audit of all 930 tracked files plus the 173-commit history. The working tree was in good shape; the gates protecting it were not running. Three verified failures, each proven by running the thing rather than reading it: 1. pii-gate in CI passed while checking zero identity terms. It runs test-no-secrets.sh on every PR, but no denylist exists on a CI runner, so identity_denylist_check took its "none configured" branch and counted that as PASS. Running the suite locally with the denylist hidden printed the identical "27 passed, 0 failed" as a run that really checked 25 terms. A check that verifies nothing must not report PASS: that branch now reports SKIP, the summary names the skip count, and OPS_PII_DENYLIST_REQUIRED=1 turns it into a hard failure for anyone who wants it enforced. 2. .githooks/pre-commit was dead code. Present, executable, never invoked — git only runs hooks from core.hooksPath or .git/hooks and nothing set either, in the primary checkout or any worktree. Every commit in this repo's life bypassed it. Added bin/ops-install-git-hooks to wire it, which also warns when no denylist is configured. Verified by staging a line containing a real denylist term: the commit was refused and no commit object was created. 3. test-hermes-plugin.sh hardcoded the operator's private domain into the detector that exists to keep private domains out of this repo. That is the same failure as the audit doc quoting what it scrubbed. Generic shapes stay inline; machine-specific terms now load from the out-of-repo denylist. Verified in both directions: planting a real term fails the test, removing it passes. Also: docs/PII-AUDIT.md still named the operator's primary company in the row explaining that the name is load-bearing, so Round 3's own lesson had not been applied to Round 3. Described by class now. A private hostname in an outbound-guard test comment is genericized. Round 4 of the audit is recorded in docs/PII-AUDIT.md, including what was deliberately NOT done: main has no branch protection (owner settings, not code), and history is not rewritten. 14 of 25 terms appear across 34 paths in history, and with 21 forks created both before and after every scrub a rewrite reaches none of them while breaking every open PR and clone. Rotate what matters; treat anything that ever appeared here as public. Verified: test-no-secrets 27, test-skills-lint 791, test-plugin-validate 7, test-bin-scripts 267 — all passing, zero failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A denylist can only hold the operator's own terms, so it structurally cannot catch another organisation's identifiers. Ten client Linear UUIDs, a client team key in ~25 places plus a filename, and a set of that client's real issue ids shipped in this repo because no rule forbade them. Rule 0 now covers third-party data explicitly. - alignment_lib: CLIENT_TEAM_KEY is the single source, read from LINEAR_CLIENT_TEAM_KEY, default placeholder "TEAM". - delegate bridge: imports it instead of redefining it (the local duplicate was missing, so the module raised NameError on import), team map comes from LINEAR_TEAM_MAP_JSON, canon fallbacks emptied. - mirror + fix-all: same import; hea_linear_fix_all.py renamed to linear_fix_all.py, env vars HEA_LINEAR_FIX_ALL_* to LINEAR_FIX_ALL_*. - inbox digest: timestamp zone from OPS_TZ, host zone when unset (TZ="" would have forced UTC, not the host zone). - marketing provision: timeZone and currencyCode from OPS_TZ/OPS_CURRENCY. - assets: an unreferenced brand-named logo renamed to ops-pixel-logo.svg. - docs: LOCAL-PREFS gains a third-party identifier table, PII-AUDIT gains round 5 including the two agent findings that did not survive verification and why `paperclip` was deliberately left alone. Tests: tests/run-all.sh 45 suites, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ook actually block A PII denylist can only ever hold the operator's own terms; a hardcoded list of anyone else's identifiers would itself be the leak. So it structurally cannot catch a third party's workspace UUIDs, team key, or issue ids — nobody can enumerate them in advance. The scanner was not broken; it answered a different question, and reported PASS while a client's identifiers sat in a public repo. Invert the rule for identifier-shaped literals: every UUID and every issue-tracker-shaped key in the tree FAILS unless it is listed in tests/known-public-constants.txt with a stated reason. An IANA timezone in code or config fails outright. Pasting a client's UUID now costs a line in a diff, argued for in front of a reviewer. Three properties make it hold: - zero configuration, so it runs on the CI path where every denylist check SKIPs - it sweeps every tracked file including tests/, which EXCLUDE_DIRS drops — the scanner's own directory was the one unscanned place - it cannot SKIP: a check that could not run counts as a failure, because counting it as a pass is how a gate silently stops gating tests/test-pii-gate-fires.sh is the negative control — a gate nobody has watched fail is not a gate. It plants the exact leaked shapes and asserts refusal, plus a clean-tree control so a scanner that fails on everything cannot pass either. On its first run it found four defects in checks written minutes earlier, each of which would have read as PASS forever: - grep -o omits the filename with a single file, so every ':'-anchored filter silently stopped filtering (-oH) - the issue-key prefix was capped at 6 chars, so a longer key could ride in on the back of an allowlisted one - both checks skipped instead of failing on an empty file list - git rev-parse --show-toplevel returns a physical path; comparing it against a logical pwd matched nothing under a symlinked checkout, and the sweep then passed over an empty file list (pwd -P) It also caught three Europe/Amsterdam leaks the manual scrub had missed; those schedules are now stated in UTC and read the display zone from $OPS_TZ. Then the first commit of all this printed three BLOCKED lines in the pre-commit hook and landed anyway. The hook applied an amnesty to its own failure flag AFTER every check had run: if the only email hits were example domains it reset the flag to 0 and took every other failure with it. Detected, announced, waved through — a worse shape than silence, because output that reads like enforcement is why nobody looks. The example-domain filter now applies inside the email check, and nothing resets the flag afterwards. A late amnesty can only ever be broader than the check it was written for. The hook also honours the same three-file exemption as the scanner, so the negative controls can hold the shapes they forbid without --no-verify. tests/test-pre-commit-hook-blocks.sh is the negative control for the hook itself: eight cases driven through a real `git commit`, asserting the exit status rather than the output, because the exit status is the only part of a hook that stops anything. Case 6 is the regression — a forbidden identifier in the same commit as a harmless @example.com address must still be refused. Cases 7 and 8 pin the exemption to three exact paths rather than the tests/ directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wat dit doet
Twee dingen, in deze volgorde.
1. Regels en precedentie (
skills/ops-rules/SKILL.md). De nummers droegen geenprioriteit; dat staat er nu expliciet. Regel 0 krijgt een clausule erbij:
Een denylist bevat per definitie de eigen termen van de operator. Daarmee kan hij
de UUID's, team-keys en issue-ids van een klant structureel niet vangen. Die
clausule benoemt ook de tell: een id dat uit
os.environkomt met vijf hardcodedbroertjes eronder is een halfafgemaakte migratie.
2. De opruiming die daaruit volgde. De regel legde echte data bloot in deze
publieke repo:
LINEAR_TEAM_MAP_JSONLINEAR_CLIENT_TEAM_KEY, defaultTEAMhea_thrash_canons-module buiten de repoOPS_TZ, anders de zone van de hostOPS_CURRENCYCLIENT_TEAM_KEYwoont inalignment_lib.pyen wordt overal geïmporteerd.hea_linear_fix_all.pyheet nulinear_fix_all.py, metLINEAR_FIX_ALL_*alsenv-prefix.
Bijvangst
linear_paperclip_delegate_bridge.pygooide eenNameErrorbij import: delokale
CLIENT_TEAM_KEYwas in een eerdere ronde verwijderd zonder vervanging.Alle vier de modules laden nu.
Niet gedaan, bewust
paperclipblijft staan, tegendocs/public-template-contract.md:34in. Van de272 voorkomens zijn er 45 wire-format:
[Paperclip <id>],paperclip-export:,paperclip-comment:worden in een levende Linear-workspace geschreven en daarweer uit geparsed. Alleen de lezer hernoemen breekt pair-herkenning stil. Dat
vraagt een eigen migratie met een dubbele-spelling-venster. Geverifieerd dat er
geen
PAPERCLIP_*in de secret-stores staat, dus er breekt vandaag niets.Documentatie
docs/LOCAL-PREFS.mdkrijgt een tabel voor identifiers van derden.docs/PII-AUDIT.mdkrijgt ronde 5, inclusief de twee bevindingen van deaudit-agent die de verificatie niet overleefden (de "private MCP hostname"
bestaat niet in de tree; het "tailnet CGNAT IP" is
100.64.0.10, hetconventionele voorbeeldadres dat de sanitizer in zijn eigen tests gebruikt).
Tests
tests/run-all.sh: 45 suites, 0 failed.test-no-secrets.sh28/0.De pre-commit hook blokkeerde de eerste poging op mijn eigen audit-notitie, die
de oude bestandsnaam letterlijk citeerde. Terecht; geredigeerd.