From fac543af4b5f2e8eea9383447d339e61637a7bf6 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:54:35 -0400 Subject: [PATCH 1/4] fix(claude-config): defer hook blockability to the upstream per-event exit-2 table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exit-2 bullet in audit-instructions' conflict-criteria.md split hook events into six "blockable" and five "non-blockable" names and read as exhaustive. The hooks page's "Exit code 2 behavior per event" table documents far more, and this repository's own hooks register five events in neither list — ConfigChange and PostToolBatch block on exit 2, while StopFailure, PermissionDenied, and InstructionsLoaded have their exit code ignored — so an auditor holding this file could not grade any of them. The bullet now names that table as the sole authority and restates none of its rows: resolve the handler's event, read its row, pair on the row's own `Can block?` cell, and take the paired content from what the row states is prevented rather than assuming a tool call or a prompt. An event with no row, or an unreachable table, is recorded as `blockability-unresolved` on the same terms as the file's existing text-unresolved rule instead of being inferred. The SubagentStop subagent-scoping rule and the PostToolUse/PreToolUse worked pair survive as examples, so the criterion keeps its detection function. The recheck trigger no longer fires on a row added upstream, and eval 16 tests the lookup procedure rather than the memorized split. Refs #1989 (row 244) Co-Authored-By: Claude Opus 5 (1M context) --- .../claude-config/.claude-plugin/plugin.json | 2 +- plugins/claude-config/CHANGELOG.md | 19 ++++++++ .../audit-instructions/evals/evals.json | 4 +- .../reference/conflict-criteria.md | 46 +++++++++++-------- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/plugins/claude-config/.claude-plugin/plugin.json b/plugins/claude-config/.claude-plugin/plugin.json index f39fde507e..ae27edebf9 100644 --- a/plugins/claude-config/.claude-plugin/plugin.json +++ b/plugins/claude-config/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-config", - "version": "0.25.0", + "version": "0.25.2", "description": "Six configuration-health skills (plus setup) for a repo's Claude Code configuration: audit (settings.json / .mcp.json / hooks / plugins / permissions drift), audit-automation-gaps (evidence-gated verdicts on automation gaps), audit-permission-grants (allow-rule / allowed-tools grants for auto-mode durability and portability), audit-instructions (locally-owned instruction surfaces vs current model capability — proposes removals/rewrites of instructions the model no longer needs, and detects cross-surface instruction conflicts), audit-pass (one coordinated, ordered, resumable pass over a named target — three-scope inventory, run-time-derived exclusion set, stable finding identity, suppression memory, resume, one human gate — delegating every check to the plugin that owns it), and unhobble (the empirical bare-baseline experiment: reversibly strip a repo's standing instructions, log real stumbles against the current model, re-add only what evidence earns).", "author": { "name": "Melodic Software", diff --git a/plugins/claude-config/CHANGELOG.md b/plugins/claude-config/CHANGELOG.md index 84edda620c..61c19bebef 100644 --- a/plugins/claude-config/CHANGELOG.md +++ b/plugins/claude-config/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to the `claude-config` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.25.2] + +### Fixed + +- **`audit-instructions`: the hook-event blockability partition in `conflict-criteria.md` was + closed** (conflict-criteria 1.3.0 → 1.4.0; issue #1989 row 244). The exit-2 bullet enumerated six + "blockable" and five "non-blockable" events as an exhaustive split, while the hooks page's + "Exit code 2 behavior per event" table documents far more — including five events this repository's + own hooks already register (`ConfigChange` and `PostToolBatch` block; `StopFailure`, + `PermissionDenied`, and `InstructionsLoaded` have their exit code ignored), every one of them + ungradeable under the old text. The bullet now defers to that table as the sole authority and + restates none of its rows: resolve the handler's event, read its row, and pair on the row's own + `Can block?` cell — taking the paired content from what the row states is prevented rather than + assuming a tool call or a prompt, and recording an event with no row (or an unreachable table) as + `blockability-unresolved` instead of inferring it. The `SubagentStop` subagent-scoping rule and + the `PostToolUse`/`PreToolUse` worked pair are kept as examples. The file's recheck trigger no + longer fires on a row added to the upstream table, and `evals/evals.json` eval 16 now tests the + lookup procedure rather than the memorized split. + ## [0.25.0] ### Added diff --git a/plugins/claude-config/skills/audit-instructions/evals/evals.json b/plugins/claude-config/skills/audit-instructions/evals/evals.json index 8141abedd4..525b6fc65c 100644 --- a/plugins/claude-config/skills/audit-instructions/evals/evals.json +++ b/plugins/claude-config/skills/audit-instructions/evals/evals.json @@ -187,11 +187,11 @@ "id": 16, "name": "post-tool-use-exit-2-is-not-a-gate", "prompt": "/claude-config:audit-instructions conflicts — my settings.json has a PostToolUse hook on Write that runs a linter and exits 2 with \"markdown lint failed: do not write this file\" on stderr, and my project CLAUDE.md instructs Claude to use the Write tool to create new docs. Is that a conflict?", - "expected_output": "Reports NO conflict, and gives the event-specific reason. Exit 2 is turn-scoped error feedback, never a standing directive, and whether it also carries a gate depends on the event: the hooks page's per-event exit-2 table makes PostToolUse non-blocking — the tool already ran — alongside Notification, SubagentStart, SessionStart, and SessionEnd, while exit 2 does block on PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact, and UserPromptExpansion. Because PostToolUse blocks nothing, there is no act for the stderr to stand in for and no gate to pair, so the message is transient feedback that pairs as nothing. Treating the stderr as a prohibition on Write would manufacture an unsatisfiable conflict against a CLAUDE.md that requires Write — the write already happened and the hook can neither block nor undo it. Had the same handler been registered on PreToolUse, it WOULD enter the comparison set as the act it blocks.", + "expected_output": "Reports NO conflict, and gives the event-specific reason. Exit 2 is turn-scoped error feedback, never a standing directive, and whether it also carries a gate depends on the event, resolved by reading that event's row in the hooks page's \"Exit code 2 behavior per event\" table rather than from a memorized set of events. PostToolUse's row says it cannot block — the tool already ran — so there is no act for the stderr to stand in for and no gate to pair, and the message is transient feedback that pairs as nothing. Treating the stderr as a prohibition on Write would manufacture an unsatisfiable conflict against a CLAUDE.md that requires Write — the write already happened and the hook can neither block nor undo it. Had the same handler been registered on PreToolUse, whose row blocks the tool call, it WOULD enter the comparison set as the act that row states is prevented.", "files": [], "expectations": [ "Reports no conflict, on the ground that PostToolUse exit 2 blocks nothing because the tool already ran — not by treating every exit-2 stderr message as the act it blocks", - "Resolves blockability from the event before applying the gate abstraction, and gets the partition right: PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact, and UserPromptExpansion block, while PostToolUse, Notification, SubagentStart, SessionStart, and SessionEnd do not", + "Resolves blockability from the handler's own event row in the upstream per-event exit-2 table before applying the gate abstraction, rather than from a fixed list of blocking and non-blocking events, and takes the paired content from what that row states is prevented", "States that the same handler on a blockable event such as PreToolUse would enter the comparison set as the act it blocks, so the non-blocking case is a scoped exclusion rather than a blanket exemption for exit-2 hooks" ] } diff --git a/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md b/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md index 14af61a286..87272f32e9 100644 --- a/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md +++ b/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md @@ -1,7 +1,7 @@ # Cross-Surface Conflict Criteria -Version: 1.3.0 -Last updated: 2026-07-31 +Version: 1.4.0 +Last updated: 2026-08-08 **The adjudication procedure for check I15.** [criteria.md](criteria.md)'s I15 entry owns the definition — what a cross-surface conflict *is*, its comparison set, its import and symlink @@ -19,8 +19,10 @@ precedence or load-order text; a change to the skills page's statements about in any new instruction surface added to the product; a change to how permission rules or permission modes remove a tool from Claude's pool; a change to **which hook events inject handler output into the session's context**, to the events `additionalContext` is accepted on, or to the handler types -that can return it; a change to **which events exit 2 can actually block** in the hooks page's -per-event exit-2 table, or to the set of locations a hook may be declared in. +that can return it; the removal, renaming, or restructuring of the hooks page's **per-event exit-2 +table** this file defers to for blockability, or a change to the set of locations a hook may be +declared in. A row added to or changed within that table needs no recheck — nothing here restates +its rows. ## Sources @@ -208,21 +210,29 @@ Three consequences for residency, and each one bounds a pair rather than admitti - **Exit-2 stderr is turn-scoped error feedback, not a standing directive — and only some events have an act to block.** "Exit 2 means a blocking error … stderr text is fed back to Claude as an error message." It reaches Claude, so it is not nothing; but it is a one-turn message, never a - standing rule. Whether it also carries a *gate* is event-specific, and the hooks page's per-event - exit-2 table settles it — resolve the handler's event before applying any gate abstraction: - - **Blockable events** — `PreToolUse`, `UserPromptSubmit`, `Stop`, `SubagentStop`, `PreCompact`, - and `UserPromptExpansion`. Here exit 2 does prevent something, so the conflict-bearing content - is the act it blocks — the treatment the prompt-hook bullets above already give. `SubagentStop` - is blockable but subagent-scoped: its act pairs inside the subagent, under the subagent-scoping + standing rule. Whether it also carries a *gate* is event-specific, and the sole authority on that + is the hooks page's + [Exit code 2 behavior per event](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event) + table. Its rows are not reproduced here in either direction: the event set grows, so any list + copied into this file becomes a closed partition that silently misgrades the next event added. + Resolve the handler's event, read that event's row, and pair on the row's own `Can block?` cell: + - **The cell says yes** — the conflict-bearing content is whatever that row states is prevented, + quoted from the row rather than assumed. What a row prevents is not always a tool call or a + prompt; the cell, never the gate abstraction, supplies the paired content. `SubagentStop` + blocks but is subagent-scoped: its act pairs inside the subagent, under the subagent-scoping rule above, and never against a main-session surface. - - **Non-blockable events** — `PostToolUse`, `Notification`, `SubagentStart`, `SessionStart`, and - `SessionEnd`. Nothing is prevented, so there is no act and no gate to pair; the table says so - outright for `PostToolUse` ("the tool already ran"), and this repository's own `PostToolUse` - linter records the same thing at `plugins/actionlint/hooks/actionlint-check.sh`. Treat the - message as transient feedback and pair it as nothing. Reading a `PostToolUse` linter's exit-2 - stderr as a prohibition on the tool it ran *after* would manufacture an unsatisfiable conflict - against any instruction requiring that tool — the tool already ran, and the hook can neither - block nor undo it. + - **The cell says no** — nothing is prevented, so there is no act and no gate to pair; treat the + message as transient feedback and pair it as nothing. `PostToolUse` is the worked example: its + row says so outright ("the tool already ran"), and this repository's own `PostToolUse` linter + records the same thing at `plugins/actionlint/hooks/actionlint-check.sh`. Reading that linter's + exit-2 stderr as a prohibition on the tool it ran *after* would manufacture an unsatisfiable + conflict against any instruction requiring that tool — the tool already ran, and the hook can + neither block nor undo it. Registered instead on `PreToolUse`, whose row blocks the tool call, + the same handler WOULD enter the comparison set as the act it blocks. + - **The event has no row, or the table could not be reached** — record the surface with its event + as `blockability-unresolved` and report the pair as such, on the same terms the + `text-unresolved` rule below gives. Never infer blockability from the event's name, its prefix, + or from what a hook of that shape usually does. - **A hook's own configuration is still not instruction text.** The command line, its arguments, and its `matcher` are the gate, not prose addressed to the model. Extract only what is injected, under the same no-secrets handling every settings-sourced surface gets. From cc2a4d352ff10dcd26173e9c97852738b65e9261 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:11:26 -0400 Subject: [PATCH 2/4] fix: cite actionlint-check.sh for what it demonstrates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PostToolUse worked example read the hook as an exit-2 emitter; it is advisory by design — always exit 0, findings via additionalContext — because exit 2 there could block nothing. The citation now states that, and the exit-2 hypothetical is generic rather than pinned to this hook. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HbtPzLRe1yBavNpsmv5Tum --- .../reference/conflict-criteria.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md b/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md index 23edf8bc12..14afda4c31 100644 --- a/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md +++ b/plugins/claude-config/skills/audit-instructions/reference/conflict-criteria.md @@ -225,11 +225,13 @@ Three consequences for residency, and each one bounds a pair rather than admitti - **The cell says no** — nothing is prevented, so there is no act and no gate to pair; treat the message as transient feedback and pair it as nothing. `PostToolUse` is the worked example: its row says so outright ("the tool already ran"), and this repository's own `PostToolUse` linter - records the same thing at `plugins/actionlint/hooks/actionlint-check.sh`. Reading that linter's - exit-2 stderr as a prohibition on the tool it ran *after* would manufacture an unsatisfiable - conflict against any instruction requiring that tool — the tool already ran, and the hook can - neither block nor undo it. Registered instead on `PreToolUse`, whose row blocks the tool call, - the same handler WOULD enter the comparison set as the act it blocks. + is built on that row — `plugins/actionlint/hooks/actionlint-check.sh` deliberately always + exits 0 and surfaces findings as advisory context, because an exit 2 there could block + nothing. Reading any `PostToolUse` handler's exit-2 stderr as a prohibition on the tool it ran + *after* would manufacture an unsatisfiable conflict against any instruction requiring that + tool — the tool already ran, and the hook can neither block nor undo it. Registered instead on + `PreToolUse`, whose row blocks the tool call, the same handler WOULD enter the comparison set + as the act it blocks. - **The event has no row, or the table could not be reached** — record the surface with its event as `blockability-unresolved` and report the pair as such, on the same terms the `text-unresolved` rule below gives. Never infer blockability from the event's name, its prefix, From c383014540dbe4c0c86c68f74d5ccb3ae8f85bb6 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:22:48 -0400 Subject: [PATCH 3/4] fix(claude-config): resolve changelog conflict left in previous merge commit The prior merge commit pushed a CHANGELOG.md still carrying conflict markers and the 0.27.1 heading while plugin.json claimed 0.27.2. The row-244 entry now heads the file as 0.27.2 above main's 0.27.1 listing-trim entry. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HbtPzLRe1yBavNpsmv5Tum --- plugins/claude-config/CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/claude-config/CHANGELOG.md b/plugins/claude-config/CHANGELOG.md index 28ae55a45b..f3b249c6ba 100644 --- a/plugins/claude-config/CHANGELOG.md +++ b/plugins/claude-config/CHANGELOG.md @@ -3,9 +3,8 @@ All notable changes to the `claude-config` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. -## [0.27.1] +## [0.27.2] -<<<<<<< HEAD ### Fixed - **`audit-instructions`: the hook-event blockability partition in `conflict-criteria.md` was @@ -22,15 +21,15 @@ All notable changes to the `claude-config` plugin are documented here. Format fo the `PostToolUse`/`PreToolUse` worked pair are kept as examples. The file's recheck trigger no longer fires on a row added to the upstream table, and `evals/evals.json` eval 16 now tests the lookup procedure rather than the memorized split. -||||||| 75b0c7b9 -======= + +## [0.27.1] + ### Changed - **`audit-instructions`: listing description tightened (1,197 → 948 chars)** — trimmed the explanatory prose from the frontmatter `description` toward the shared skill-listing budget (claude-code-plugins#2022, option 2). Every single-quoted trigger phrase is preserved verbatim (skill-quality check 3); the audit's scope and report-only contract are unchanged in the body. ->>>>>>> origin/main ## [0.27.0] From a03855dde1fdac99727464154c6b86219c624d08 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:31:26 -0400 Subject: [PATCH 4/4] fix(claude-config): resolve the changelog conflict and land row 244 as 0.27.3 The previous merge commit again left conflict markers in CHANGELOG.md while plugin.json moved to 0.27.3 (main shipped its own 0.27.2 mid-flight). The row-244 entry now heads the file as 0.27.3; main's 0.27.2 entry is preserved byte-identically beneath it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HbtPzLRe1yBavNpsmv5Tum --- plugins/claude-config/CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/claude-config/CHANGELOG.md b/plugins/claude-config/CHANGELOG.md index 4cdbf7f713..e70c87555a 100644 --- a/plugins/claude-config/CHANGELOG.md +++ b/plugins/claude-config/CHANGELOG.md @@ -3,11 +3,10 @@ All notable changes to the `claude-config` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. -## [0.27.2] +## [0.27.3] ### Fixed -<<<<<<< HEAD - **`audit-instructions`: the hook-event blockability partition in `conflict-criteria.md` was closed** (conflict-criteria 1.3.0 → 1.4.0; issue #1989 row 244). The exit-2 bullet enumerated six "blockable" and five "non-blockable" events as an exhaustive split, while the hooks page's @@ -22,8 +21,11 @@ All notable changes to the `claude-config` plugin are documented here. Format fo the `PostToolUse`/`PreToolUse` worked pair are kept as examples. The file's recheck trigger no longer fires on a row added to the upstream table, and `evals/evals.json` eval 16 now tests the lookup procedure rather than the memorized split. -||||||| b77782a0 -======= + +## [0.27.2] + +### Fixed + - **`conflict-scan.sh`: the coordinated-directive boundary honored only a subset of the mandate tokens, so the most common phrasing silently dropped real conflicts.** `COORD_ERE` carried a hand-copied token list that had fallen behind `MANDATE_ERE`: `use`, `present`, and `ask` were in @@ -94,7 +96,6 @@ All notable changes to the `claude-config` plugin are documented here. Format fo mid-run on a bare `command not found` rather than on a named prerequisite. Only `conflict-scan.sh` probes and `exit 2`s. Both surfaces now name all three skills, and the README's requirements section names `awk` and `sort` alongside `jq` and `curl`. ->>>>>>> origin/main ## [0.27.1]