Repository navigation
Permission-posture claims restated across unsynced surfaces — enumeration-based containment cannot close the class #353
Description
Root cause
Claude Code permission-engine posture — what allowed-tools does, what a settings allow-rule does, where a local-review provider grant may live — is authored independently on N surfaces and kept correct by restatement. Every containment attempt so far has been an ENUMERATION at a different layer. An enumeration can only be complete with respect to what its author already knew; the next surface, the next paraphrase, or the next unrouted path reopens the class.
Four consecutive review iterations on one PR each found a DIFFERENT member. That is the signature of complete-the-known-set, not of a closed guard.
root_class: permission-posture-restated-across-unsynced-surfaces
Evidence — four iterations, four members, one branch
| # | Commit | Member found | Enumeration layer that failed |
|---|---|---|---|
| 1 | 3510a2e |
seed-hive template seeded a permanently dead Bash(node /path/to/...) grant into a consumer's COMMITTED settings; a single-quoted heredoc froze the doc placeholder into runtime data |
posture restated in the template, unverified against the artifact |
| 2 | 98d98db, 4c486f5 |
README / CLAUDE.md / web/functionality.html out of sync with each other, and carrying copilot-provider claims not shipped in the release |
enumerate the SURFACES that carry the literal (set_check equal mode over 3 named files) |
| 3 | 0b6b50a |
permission-engine rationale restated in CLAUDE.md and web; single-homed onto ADR-0010; validate.sh routing added |
enumerate the EXEMPTIONS (CHECK15_ALLOWLIST) |
| 4 | ca7c7b2, 02139a2 |
CHECK 15's scan reached files validate.sh did not route; the new AUTHORITY sentence was violated by the very surfaces exempted from the check; the allowlist prefix-matched where its header promised exact; the check advertised FAIL-CLOSED but failed OPEN at EOF |
enumerate the COVERAGE (map_path legs) and the TRIGGER (one literal token) |
Corroborating detail: review cycle 4 enumerated three allowlisted surfaces asserting engine behavior. There are four — plugin/governance/security-policy.md:124 also carries the token. Whether :124 counts as an "engine behavior assertion" or a structural-containment claim is precisely the judgment an enumerated guard cannot make. The enumeration of the violations was itself incomplete.
What a genuinely class-eliminating fix would need
A different primitive than any tried. Every attempt to date compares a surface against a LIST:
- attempt 1 — list of surfaces that must carry the literal (
safety-local-review-grant-single-home.json) - attempt 2 — list of surfaces exempt from the claim ban (
CHECK15_ALLOWLIST) - attempt 3 — list of paths the validator routes (
map_pathlegs) - attempt 4 — list of one trigger token (
CHECK15_TOKEN = 'allowed-tools'), which CHECK 15's own comment concedes is defeated by any paraphrase
A closing fix must instead compare a surface against a SOURCE. Two candidate primitives, neither yet attempted:
- Single-source + transclusion. Permission-posture prose exists in exactly ONE file. Every other surface carries a generated include region; a check regenerates and diffs. "Out of sync" stops being expressible, and the paraphrase residual dies with it, because the check compares bytes against the source rather than pattern-matching for a token.
- Derivation from ground truth. Assert the posture against the ACTUAL artifact — the seeded settings template, the agent frontmatter, the installed binary's behavior — rather than against prose in N documents. This is what would have caught member chore: address code review feedback #1 at authoring time.
Distinguishing property to test any future proposal against: does it compare a surface to a SOURCE, or to a LIST? If a LIST, it is another iteration of this class.
Open engine question (blocks part of the cluster)
Whether a skill's allowed-tools entry — e.g. Bash(node *) — suppresses the permission prompt for a provider binary invoked via node, including in a subagent context, is NOT established.
Recorded as a NON-FINDING in docs/adr/0010-permission-allowlist-posture.md, which forbids any surface from stating an answer until it is verified empirically against an installed Claude Code binary and the official permissions documentation.
Resolving this empirically would remove the standing reason several surfaces keep getting re-edited. It is a prerequisite for candidate primitive 2 on this specific claim, and is independently worth doing.
Linked surfaces
docs/adr/0010-permission-allowlist-posture.md— Findings (authority home + NON-FINDING)docs/adr/0013-decoupled-leaf-pipeline-skills.md:31docs/adr/0018-declarative-workflow-state-machine.md:69plugin/governance/security-policy.md:124,:148README.md,CLAUDE.md,web/functionality.html— the three-surface grant literalplugin/skills/_shared/settings-merge.sh— the permission template and itsNO-PROVIDER-GRANTinvarianttools/policy_check.sh— CHECK 15tools/validate.sh—map_pathtests/policy/safety-local-review-grant-single-home.jsontests/policy/safety-permission-engine-claim-containment.json
Bounded impact — why this is deferred, not fixed now
Blast radius is documentation and CI accuracy only. Across all four iterations:
- No runtime behavior change in the shipped plugin. No public API, no packaged output, no compatibility contract.
- No security boundary. Every claim in question concerns PROMPT SUPPRESSION, never a grant or a deny. An allow-rule pre-approves and never denies; absence yields a prompt, not a refusal. A wrong doc costs a contributor one extra permission prompt.
- No data path, no user-facing surface.
Worst realistic case today: (a) a doc asserts an unverified engine behavior a contributor acts on and is mildly inconvenienced; (b) a CI guard advertises reach it does not have, so a violation authored in an unrouted doc is caught at push-to-main rather than at PR. Cycle 4 closes (b) for the .md/.html axis.
The fix-now/defer split: cycle 4 landed the four concrete correctness defects because they are bounded, verified green-safe, and each is a few lines. The structural primitive swap is deferred here because it is a design change touching a dozen surfaces plus a build/generation step, it would be the fifth structural attempt inside a patch release, and the impact it mitigates is documentation accuracy — not a defect any consumer can hit.
Residuals carried here, nothing dropped
The cluster members NOT fixed in cycle 4, all recorded above:
- The paraphrase residual — CHECK 15 triggers on one literal token, so a restatement that never spells
allowed-toolspasses green. Accepted P17 non-mechanizable residue. - The surface-to-list comparison primitive — the actual root.
- The open engine question.
- A widening authored without the
# CHECK15-ALLOWmarker, or outside theCHECK15_ALLOWLISTarray, is invisible to the line-scoped marker-anchored pin. - CHECK 15 neutered internally while its header, emitter and allowlist literals all stay intact.
Introduced by PR for fix/seed-hive-dead-codex-grant-a7f2 (plugin 2.40.10).
Activity
revert(policy): remove CHECK 15 pending a source-derived successor (#353
fix(seed-hive): stop seeding a dead codex permission grant into commi…
brenpike commentedon Sep 18, 2026
New member of this class: governance-enumeration restatement in the remediation doctrine
Surfaced by the pre-PR adversarial review loop on branch
feat/359-recorded-disposition(implementing #359). Filing here rather than opening a new issue: this is the sameroot_classfamily already tracked by this issue, and the doctrine's own Defer-with-Scope default is to prefer an existing tracked home.Root, sharpened
The generalisation that fits the evidence is not "the enumeration was replicated". It is:
plugin/governance/remediation-doctrine.mdbriefly carried: "The admissible destinations are enumerated ONCE — here — and no other section, agent, or skill restates them."That claim was false at the instant of introduction.
plugin/agents/overlord.md:123, authored on the same branch one commit earlier, contained the exact string the rule quotes as its forbidden example. Its truth could only ever be established by a hand-built inventory taken at authoring time, and it is falsified by the next sentence anyone writes.This also explains why the first structural fix on this surface failed to close the cluster: re-keying the concept from a destination type to a role changed the value being asserted, not the assertion form. The assertion form was the primitive.
Contrast
remediation-doctrine.mdlines 202 and 248, which make the same shape of claim but bounded to two named consumers. A bounded claim is fixture-pinnable; the unbounded one is pinnable by nothing.Linked threads
Cluster members from the review loop:
1ab1893ceb3a45968680c20facecbad99ee1ebfbda5d063ff5f5ead35d833323(high) — the unbounded claim itselfdd21cbd514684ceb465566b52fe72af079cce12f157bc53472e251a5a67e0e02(medium) — the doctrine restating a downstream payload shape, which is the same root one step out49e9482bf92c57156e69023be0b00ee97caadedc637610ccc915c6d56a8adb73— the iteration-1 memberFixed on that branch
The two live defects and the assertion form itself, by deleting the claim rather than enforcing it. After that change, no sentence in
plugin/governance/asserts a fact about the content of a file it does not name — so there is nothing left to enforce and nothing left that can be false.What that closes, precisely: the doctrine shipping a claim whose truth depends on the content of files it does not name. Previously, a narrowing sentence written in some consumer made the doctrine false (self-refuting governance). Now it makes one consumer wrong — an ordinary content defect that review catches.
Deferred here
The general primitive: how a governance enumeration reaches its consumers without being hand-copied.
A token-denylist-plus-allowlist check was considered and rejected, on this issue's own disqualifying test — does it compare a surface to a SOURCE, or to a LIST? If a LIST, it is another iteration of this class. That mechanism was in fact already built as CHECK 15, hardened across three commits, and reverted at
0fa39cbpending a source-derived successor. Building it again would be the next iteration of the class, not a fix for it.Candidate primitives are unchanged: single-source plus transclusion with a byte-diff gate, or derivation from ground truth.
Site audit — six of eight reported sites are legitimate, not defects
The review loop's site list over-generalised. Audited verdicts, so a future attempt does not "fix" correct prose:
plugin/agents/overlord.md:123plugin/agents/overlord.md:121plugin/agents/cerebrate.md:260-265plugin/governance/decision-autonomy.md:44plugin/governance/decision-autonomy.md:69-70CONTEXT.md:179,:218docs/adr/0023,docs/adr/0026,CHANGELOG.mdBounded impact
Documentation and CI accuracy only. No runtime behavior, no public contract, no data path, no security boundary. Worst case: a runtime-loaded doc restates a rule that later drifts, and review catches it. It cannot fire without a human authoring a new narrowing restatement.
Note on this issue's metadata
The "Linked surfaces" section cites
tools/policy_check.shCHECK 15 andtests/policy/safety-permission-engine-claim-containment.json. Neither exists at HEAD — both were removed by the revert at0fa39cb. Worth correcting so a future reader does not go looking for them.Also noticed, not filed separately
remediation-doctrine.md:202and:248make bounded two-consumer absence claims that no fixture pins. A load-bearing claim without a test is decoration. Deliberately out of scope for the branch above — fold into this issue or split out, whichever suits.