Skip to content

Permission-posture claims restated across unsynced surfaces — enumeration-based containment cannot close the class #353

Description

@brenpike

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_path legs)
  • 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:

  1. 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.
  2. 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:31
  • docs/adr/0018-declarative-workflow-state-machine.md:69
  • plugin/governance/security-policy.md:124, :148
  • README.md, CLAUDE.md, web/functionality.html — the three-surface grant literal
  • plugin/skills/_shared/settings-merge.sh — the permission template and its NO-PROVIDER-GRANT invariant
  • tools/policy_check.sh — CHECK 15
  • tools/validate.sh — map_path
  • tests/policy/safety-local-review-grant-single-home.json
  • tests/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:

  1. The paraphrase residual — CHECK 15 triggers on one literal token, so a restatement that never spells allowed-tools passes green. Accepted P17 non-mechanizable residue.
  2. The surface-to-list comparison primitive — the actual root.
  3. The open engine question.
  4. A widening authored without the # CHECK15-ALLOW marker, or outside the CHECK15_ALLOWLIST array, is invisible to the line-scoped marker-anchored pin.
  5. 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

  1. brenpike commented on Sep 18, 2026

    @brenpike
    OwnerAuthor

    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 same root_class family 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:

    A governance sentence asserted an UNBOUNDED NEGATIVE UNIVERSAL — a claim about the content of every file in the repo, including files not yet authored.

    plugin/governance/remediation-doctrine.md briefly 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.md lines 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 itself
    • dd21cbd514684ceb465566b52fe72af079cce12f157bc53472e251a5a67e0e02 (medium) — the doctrine restating a downstream payload shape, which is the same root one step out
    • 49e9482bf92c57156e69023be0b00ee97caadedc637610ccc915c6d56a8adb73 — the iteration-1 member

    Fixed 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 0fa39cb pending 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:

    Site Verdict
    plugin/agents/overlord.md:123 defect — bound the role noun to one destination type. Fixed.
    plugin/agents/overlord.md:121 legitimate — a rendered field label, not an enumeration. Fixture-pinned.
    plugin/agents/cerebrate.md:260-265 legitimate — an acting agent that must produce the destination choice; names both, narrows neither.
    plugin/governance/decision-autonomy.md:44 legitimate — disposition 2x2 cell, a different concern; names both.
    plugin/governance/decision-autonomy.md:69-70 legitimate but heavy — a genuine second normative copy of the Recorded Residual mechanics. Not drifted. The strongest DRY smell remaining, and the natural first target if this issue is picked up.
    CONTEXT.md:179, :218 legitimate — glossary. Defining a term requires naming what it enumerates. Not runtime-loaded.
    docs/adr/0023, docs/adr/0026, CHANGELOG.md out of scope — historical records. Rewriting them would falsify the record.

    Bounded 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.sh CHECK 15 and tests/policy/safety-permission-engine-claim-containment.json. Neither exists at HEAD — both were removed by the revert at 0fa39cb. Worth correcting so a future reader does not go looking for them.

    Also noticed, not filed separately

    remediation-doctrine.md:202 and :248 make 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions