Skip to content

feat(procedures): one off-switch per gate, on by default, recorded when used - #10

Open
drewdrewthis wants to merge 7 commits into
mainfrom
feat/gate-escape-hatch
Open

feat(procedures): one off-switch per gate, on by default, recorded when used#10
drewdrewthis wants to merge 7 commits into
mainfrom
feat/gate-escape-hatch

Conversation

@drewdrewthis

@drewdrewthis drewdrewthis commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #16

What

An escape hatch for the procedures plugin's three enforcing hooks: one named boolean per gate, on by default, recorded when a gate is actually released.

userConfig option turns off
enable_how_do_i_gate how-do-i-gate.sh (PreToolUse)
enable_am_i_done_gate am-i-done-gate.sh (Stop)
enable_frontmatter_check enforce-frontmatter.sh (PostToolUse)

Each reaches the hook as CLAUDE_PLUGIN_OPTION_ENABLE_<KEY>; PROCEDURES_ENABLE_<KEY> is the plain-env equivalent for one-off invocations.

Why

Installed as a plugin you cannot edit a hook to silence it. The platform has no targeted alternative — verified against the shipped 2.1.224 binary and the docs: disableAllHooks is global (it also kills statusLine and /goal), claude plugin disable takes the skills and commands with it, and "There is no way to disable an individual hook while keeping it in the configuration." Notably skillOverrides exists for per-skill disable with no hook analogue.

Design notes

No wildcard, no comma-spec, no central registry. An enumerated list is the ts_reset bug ADR-016 documents. A parsed spec is worse here specifically: every gate fails open, so a malformed spec lands in the same drawer as lib-unreadable:* and one typo disarms all three. A per-gate boolean is fail-safe by construction.

Neither channel has precedence — either saying false turns the gate off. With precedence the plain var would be dead on an installed plugin: the option carries "default": true, so the harness exports it as true on every invocation.

The record is written at the release point. Earlier revisions checked the switch before the hook knew whether it would act, so the log counted invocations — a subagent call the gate never binds wrote a row, as did a Write to an unrelated .txt.

Only the userConfig channel is trusted. See the security section below.

Human verification (for if you really don't trust me)

  1. cd plugins/procedures && bats hooks/tests — 164 pass.
  2. Prove the switch is load-bearing, on a copy: cp -r plugins/procedures $(mktemp -d)/rev, revert the ge_enabled call in rev/hooks/how-do-i-gate.sh to the top of the file, re-run — a released gate is recorded only when it would otherwise have fired goes red.
  3. Prove the manifest contract holds both ways: on that copy, jq 'del(.userConfig)' .claude-plugin/plugin.json — tests 18, 19 and 20 all go red.
  4. Confirm the fail-safe direction: chmod 000 the escape lib on the copy and check a gate still denies.
  5. claude plugin install procedures@<marketplace> --config enable_how_do_i_gate=false then inspect pluginConfigs in ~/.claude/settings.json.

How I can prove I was successful

  • Suite green at this head — proven.

    $ cd plugins/procedures && bats hooks/tests
    ok=164  notok=0
    
  • The wiring is load-bearing, not decorative — proven (falsified on a copy). Restoring the early switch placement reddens a released gate is recorded only when it would otherwise have fired; deleting .userConfig reddens tests 18/19/20. Both mutations were run against cp -r copies, never the working tree.

  • The log now counts releases, not invocations — proven. Before the fix, a subagent payload wrote 1 row and a Write to /tmp/not-a-record.txt wrote 1 row carrying {"gate":"FRONTMATTER_CHECK",...}. Both are now pinned at zero rows by test 23.

  • The audit log is defeatable — proven, and now documented rather than claimed otherwise.

    $ GATE_ESCAPE_LOG=/dev/null PROCEDURES_ENABLE_HOW_DO_I_GATE=false bash hooks/how-do-i-gate.sh <<< '...'
    exit=0 (gate released)
    default log exists: NO
    
  • userConfig storage works — proven.

    $ claude plugin install procedures@drewdrewthis --config enable_how_do_i_gate=false
    ✔ Successfully installed plugin
    2 userConfig options not yet set — run /plugin configure …
    $ jq '.pluginConfigs' $CLAUDE_CONFIG_DIR/settings.json
    { "procedures@drewdrewthis": { "options": { "enable_how_do_i_gate": false } } }
    

    All three options are recognised and --config stores a real boolean.

  • The CLAUDE_PLUGIN_OPTION_ENABLE_* delivery path — NOT verified, and the reason is now pinned down. This is the channel that closes No way to silence one gate of the procedures plugin without uninstalling it #16 (the plain env var requires wrapping every launch), so it is the weakest point of the PR and I could not close it.

    What I tried: installed this branch into a throwaway CLAUDE_CONFIG_DIR with --config enable_how_do_i_gate=false, instrumented the cached how-do-i-gate.sh to dump CLAUDE_PLUGIN_OPTION_* to a unique marker file on entry, and drove a real interactive TUI session (tmux) — not print mode — until it executed a Bash tool call.

    Result: the tool call ran; the marker file was never written. The hook did not execute.

    This is not a plugin-loading failure, and I checked that specifically rather than assuming — in the same session:

    ❯ List the skills you have available whose name starts with procedures
    ● procedures:am-i-done
      procedures:create-new
      procedures:evolve-procedure
      procedures:how-do-i
      procedures:log
    

    Skills load; hooks do not fire. Same null in print mode earlier. enabledPlugins, pluginConfigs and the cached hooks/hooks.json were all verified present in that config.

    So: I cannot demonstrate the option reaching a hook in a redirected-config environment, and I have not isolated whether that is a CLAUDE_CONFIG_DIR limitation or something else. Everything measured in this PR runs through PROCEDURES_ENABLE_* against the real hook scripts. design-soundness confirmed from the shipped 2.1.224 binary that the manifest key → CLAUDE_PLUGIN_OPTION_<KEY> transform is byte-for-byte what gate-escape.sh reads — but that is a static trace, not a runtime observation.

    What would close it: set the option in the real ~/.claude/settings.json pluginConfigs on a machine where these hooks already demonstrably fire, and observe a gate release. I did not do that because it mutates the live gating config of this box.

Backend-only: shell hooks, a JSON manifest, and docs. No UI and no rendered surface; the terminal output above is the whole observable behaviour.

Security

The docs previously claimed a cloned repo could not disarm the host's gates, citing v2.1.207's pluginConfigs isolation. That was false for the channel this PR adds — a project's .claude/settings.json env block applies "to every session and to subprocesses Claude Code spawns from it", and a hook is such a subprocess, on every version. README, ADR-001 and the lib header now state that only CLAUDE_PLUGIN_OPTION_* carries the isolation property and that PROCEDURES_ENABLE_* is untrusted ambient config.

Neither channel is tamper-evident: whoever can set a switch can also point GATE_ESCAPE_LOG elsewhere. The log exists to show you a gate you left off months ago, not to catch an adversary — now said plainly in all three places rather than oversold.

Review

Reviewed by the standard own-PR fan-out: principles, hygiene, security, test-reviewer, plus four devils-advocate personas (Uncle Bob, Metz/Beck, Fowler, design-soundness). Five blocking threads and one New AC were raised; all are addressed in 3bf267c. See the verdict comment.

Summary by CodeRabbit

  • New Features

    • Added controls to enable or disable the how-do-I, am-I-done, and frontmatter checks.
    • Checks remain enabled by default and support explicit configuration overrides.
    • Disabled checks are recorded separately for improved visibility.
    • Improved fail-safe behavior when required tooling or configuration is unavailable.
  • Documentation

    • Updated plugin documentation and version information to explain configuration options and gate behavior.
  • Tests

    • Added comprehensive coverage for configuration precedence, defaults, logging, and failure scenarios.

…en used

Installed as a plugin, the only way to silence a gate was uninstalling the
plugin — coarser than anyone wants, and it takes the skills with it.

One named boolean per gate, declared as userConfig and read by that gate
alone:

  enable_how_do_i_gate     -> how-do-i-gate.sh
  enable_am_i_done_gate    -> am-i-done-gate.sh
  enable_frontmatter_check -> enforce-frontmatter.sh

No wildcard, no comma-spec, and deliberately no central list of the switches:
an enumerated registry is the ts_reset bug in ADR-016, and a parsed spec puts
a typo in the same drawer as a real failure — every gate here fails open, so
one bad character would disarm all three.

Either CLAUDE_PLUGIN_OPTION_ENABLE_<KEY> or PROCEDURES_ENABLE_<KEY> saying
false turns a gate off; neither wins. Precedence would make the plain var dead
on an installed plugin: the option carries "default": true, so the harness
exports it as true on every hook invocation, and a plain override losing to it
could only ever be observed in a bare checkout — the case that doesn't need it.

Recorded to its own gate-escape.jsonl, never gate-failopen.jsonl. A switched
gate is not a blind release and would destroy the fail-open rate that log
carries; but an unrecorded off-switch is invisible, and nothing would separate
an owner debugging for an hour from a child session spawned as
`PROCEDURES_ENABLE_AM_I_DONE_GATE=false claude -p …` to skip review. Both
reviewers flagged that gap independently.

Boy scout: enforce-frontmatter now resolves SCRIPT_DIR without `dirname`, like
the two gates. The escape check runs before the jq check, and that path is
exercised with PATH emptied.

Sites enumerated with:
  grep -rn "gate_failopen\|ga_binds_main" plugins/procedures/hooks/
  grep -rn "FRONTMATTER_GATE\|ge_released\|DISABLE_GATES" hooks/ README.md docs/

Tests: 19 new in hooks/tests/gate-escape.bats; suite 160 ok / 0 not ok.
Falsified by reverting the wiring — the four outcome tests go red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@drewdrewthis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 115 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 74cc8587-1407-44d0-9c2f-242d8f79e882

📥 Commits

Reviewing files that changed from the base of the PR and between 7a7ec89 and d2d2e7d.

📒 Files selected for processing (3)
  • README.md
  • plugins/procedures/.claude-plugin/plugin.json
  • plugins/procedures/hooks/how-do-i-gate.sh

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f5c3740a-ce05-406a-8265-82711116c2bc

📥 Commits

Reviewing files that changed from the base of the PR and between b710ca8 and 7a7ec89.

📒 Files selected for processing (2)
  • plugins/procedures/hooks/lib/gate-failopen.sh
  • plugins/procedures/hooks/tests/gate-escape.bats

📝 Walkthrough

Walkthrough

The procedures plugin version is updated to 0.3.0. Three enforcing hooks gain independent, enabled-by-default off-switches, fail-safe release handling, and separate escape telemetry. Documentation and Bats tests cover configuration, trust boundaries, wiring, and degraded paths.

Changes

Procedures gate controls

Layer / File(s) Summary
Configuration contract
plugins/procedures/.claude-plugin/plugin.json, docs/adrs/001-procedural-knowledge-system.md, README.md
The manifest declares three enabled-by-default boolean gate options. The ADR and README document switches, release logging, environment overrides, and trust boundaries.
Gate release implementation
plugins/procedures/hooks/lib/gate-escape.sh, plugins/procedures/hooks/lib/gate-failopen.sh, plugins/procedures/hooks/*-gate.sh, plugins/procedures/hooks/enforce-frontmatter.sh
The shared library applies explicit-off parsing, records disabled gates, and delegates enabled gates to fail-open handling. The hooks use deferred release checks and preserve enforcement when the library is unavailable.
Gate escape validation
plugins/procedures/hooks/tests/gate-escape.bats
Bats tests cover defaults, isolation, precedence, telemetry, manifest wiring, unreadable libraries, firing conditions, and degraded paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to 7a7ec

The change adds configurable per-gate escape switches with recording behavior, and no actionable merge-blocking risk remains beyond a localized provenance follow-up.

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Claude
  participant GateHook
  participant gate_escape.sh
  participant gate-escape.jsonl
  participant gate-failopen.jsonl
  Claude->>GateHook: invoke enforcing hook
  GateHook->>gate_escape.sh: evaluate gate option
  alt gate explicitly disabled
    gate_escape.sh->>gate-escape.jsonl: record gate release
    gate_escape.sh-->>GateHook: exit successfully
  else gate enabled
    gate_escape.sh->>gate-failopen.jsonl: record fail-open release when needed
    gate_escape.sh-->>GateHook: continue enforcement or fail open
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: one default-enabled, recorded off-switch per procedures gate.
Linked Issues check ✅ Passed The changes address all acceptance criteria for independent, fail-safe gate switches, separate release logging, documentation, and manifest-hook contract checks [#16].
Out of Scope Changes check ✅ Passed The documentation, version updates, hook changes, helper library, fallback, and tests directly support the linked issue objectives.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/gate-escape-hatch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gate-escape-hatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ends on

security-reviewer: the "a cloned repo cannot disarm your gates" property holds
only from Claude Code v2.1.207, where pluginConfigs stopped being read from
project-level settings. The manifest cannot express a version floor and nothing
checks it at runtime, so the docs are the enforcement. It was stated only in a
comment inside a sourced lib — not where someone installing the plugin looks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@drewdrewthis drewdrewthis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Own-PR review fan-out: principles, hygiene, security, test-reviewer, plus 4 devils-advocate personas (Uncle Bob, Metz/Beck, Fowler, design-soundness). Every blocking finding below was reproduced against the real hooks before being posted — the reproduction command is in each thread. Verdict comment follows.

Comment thread README.md Outdated
Comment thread plugins/procedures/hooks/lib/gate-escape.sh
Comment thread plugins/procedures/hooks/how-do-i-gate.sh Outdated
Comment thread plugins/procedures/hooks/tests/gate-escape.bats
Comment thread plugins/procedures/hooks/tests/gate-escape.bats
Comment thread plugins/procedures/hooks/tests/gate-escape.bats
@drewdrewthis

drewdrewthis commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review verdict: READY

Reviewed at: d2d2e7d · Run: review (own-PR) · four rounds, 11 reviewers in round 1

No blocking concerns. All review threads resolved at this SHA, and the final scoped gate returned NO BLOCKING DEFECTS, having mutation-tested each fix on a copy.

READY here means no unresolved blocking review threads. It is not merge-readiness, and one substantive gap is declared below rather than closed.

What the rounds found

Round 1 raised five blocking threads and one New AC. Round 2 found the round-1 fix had not reached two sites of the same defect class:

  • enforce-frontmatter evaluated the switch above the record-store predicate — which lives inside the linter — so with the check off, every CLAUDE.md, README.md and SKILL.md under the root logged a release nothing would have blocked.
  • gate_failopen never returns, so every degraded path pre-empted the switch entirely and filed a deliberate release as a blind fail-open — one contaminating row per tool call, in the exact rate that log exists to measure, while ADR-001 and README asserted the opposite.

Round 3 pinned the fallback shim (deleting it survived all 165 tests while breaking the degraded path — an undefined function returns 127 and execution falls through into the deny) and corrected two hook comments that had become false.

Round 4 — CodeRabbit's first pass. It skips drafts (#13), so marking this PR ready is what finally got it a read. Two findings, both valid, both fixed:

  • gate-failopen.sh — the ge_release_or_failopen shim is new behaviour in a vendored file with no upstream counterpart, but carried only an explanatory comment, not the literal # PLUGIN ADAPTATION: marker this repo's vendoring rule requires. A future vendor sync could not have told it from upstream code.
  • gate-escape.bats — the manifest contract test extracted gate keys with grep -ohP. PCRE is a GNU extension; BSD grep rejects -P, KEYS comes back empty, and the non-empty guard then fails the test for a toolchain reason while reporting a contract breach. Replaced with POSIX match-then-strip, verified byte-identical to the PCRE output, and re-falsified by mutation (a hook reading an undeclared key still reddens it).

Merged current main in

main moved 12 commits (0.2.0 → 0.3.1) while this branch sat in review and the PR went DIRTY. Resolved by merging origin/main in — not by rebase, which would have needed a force-push and discarded the review-thread anchoring. Three conflicts, all additive:

  • plugin.json — took main's release-please-managed 0.3.1, kept the three userConfig booleans. Deliberately not hand-bumped to 0.4.0: this file is versioned by release-please, so the feature bump belongs to the release commit, not to this branch.
  • README.md — kept both adaptation bullets (main's fork-skill model pin and this branch's configuration surface).
  • how-do-i-gate.sh — took main's newer deny-message wording, kept the switch guard above it. The rest of the escape-hatch wiring auto-merged.

The prior "version conflict with #9" item is now moot: #9 merged on 2026-08-07 (4cb4071).

The declared gap — unchanged, and still the thing to weigh

The CLAUDE_PLUGIN_OPTION_* delivery path is unverified, and it is the channel that actually closes #16 — the plain env var requires wrapping every launch. I drove a real interactive TUI session with the option set and the cached hook instrumented; the tool call ran and the marker was never written. That is not a plugin-loading failure: the same session listed all five procedures: skills. Skills load, hooks do not fire, under a redirected CLAUDE_CONFIG_DIR, and I did not isolate why.

Everything measured runs through PROCEDURES_ENABLE_* against the real hook scripts. What would close it is setting the option in this box's live ~/.claude settings and observing a release — deliberately not done, since it mutates live gating config.

Non-blocking (Decide / New Issue)

  • [principles] With the check off, enforce-frontmatter now forks bash + the linter on every .md write under $ROOT before discarding the result — the price of asking the record-store predicate before the switch. "Off" changed from cheap to silent. (Decide)
  • [principles] gate-escape.jsonl cannot distinguish a release the gate evaluated from one it asked about while blind. A "blind":true flag would let a consumer compute the rate. (New Issue)
  • [security] Not tamper-evident, now stated rather than oversold — whoever sets a switch can point GATE_ESCAPE_LOG at /dev/null. Tracked as Make gate-escape.jsonl tamper-evident without fixing its path #17. (New Issue)
  • [hygiene] ge__off/ge__record's double-underscore private marker has no precedent in any sibling lib. (Decide)
  • [fowler] Two hand-written JSONL appenders now share byte-identical sanitizer logic with no shared home. (New Issue)

Evidence at this SHA


Verdict is prose, not a GitHub approval. Scope: review findings only — READY means no unresolved blocking review threads at this SHA. It is not a merge-readiness signal; that is pr-ready-check.sh.

Comment thread README.md Outdated
…e docs

Addresses the five blocking threads and the New AC on #10.

RECORD AT THE RELEASE POINT. The switch was evaluated before each hook knew
whether it would have acted, so gate-escape.jsonl counted hook invocations: a
subagent call the gate never binds wrote a row, as did a Write to an unrelated
.txt. An orchestrator exporting the var once to debug produces thousands of
identical rows, which is the "gate stays off for months" failure the log was
added to prevent. The lib is still sourced early; the CALL moved below the
audience/allowlist checks (how-do-i), below the activity check (am-i-done),
and below the scope filters (frontmatter).

THE THREAT MODEL WAS FALSE. README, ADR-001 and the lib all claimed a cloned
repo could not disarm the gates, citing v2.1.207's pluginConfigs isolation.
That holds for CLAUDE_PLUGIN_OPTION_* and is irrelevant to the plain var: per
code.claude.com/docs/en/settings, a project's .claude/settings.json `env` block
applies "to every session and to subprocesses Claude Code spawns from it" — a
hook is such a subprocess, on every version. All three sites now say only the
userConfig channel carries the isolation property and name PROCEDURES_ENABLE_*
as untrusted ambient config.

NOT TAMPER-EVIDENT, now stated. GATE_ESCAPE_LOG is a plain env var and the
append is best-effort, so whoever sets a switch can also send the record to
/dev/null (verified: gate released, exit 0, no record anywhere). The log is
for finding a gate you left off, not for catching an adversary.

TESTS. `every declared enable_* option is read by a hook` passed with the whole
userConfig block deleted — a `for` over an empty key list runs zero assertions;
now asserts non-empty first. Adds the reverse sweep (a hook key absent from the
manifest fails), per-hook record content checks (a copy-paste bug hardcoding
one key would have passed), the unreadable-lib ARMED property that three hook
comments asserted and nothing pinned, and a release-vs-invocation test.

Suite 164 ok / 0 not ok. Both new behaviours falsified on a COPY of the tree
(never the working tree — that is how this worktree got wrecked mid-review):
restoring the early placement reddens the release-vs-invocation test, and
deleting userConfig reddens tests 18/19/20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude added 2 commits August 7, 2026 10:14
… directions

Two Must-Fixes from the scoped re-review. Both are the round-1 defect class
("the log counts invocations, not releases") surviving at sites the first fix
did not reach.

FRONTMATTER: the switch sat above the record-store predicate. The filters
before it are only `*.md`, `-f`, and under-$ROOT; whether a file is a RECORD is
decided inside lint-frontmatter.sh, which runs later. So with the check off,
every CLAUDE.md, README.md, agents/*.md, SKILL.md and template under the root
logged a release nothing was going to block. Measured before the fix:
$ROOT/README.md armed -> 0 rows, switch off -> 1 row. The call now lives inside
the violation branch, the one place the check would actually have fired.

FAIL-OPEN PATHS: gate_failopen never returns, so in every degraded state (no
jq, unreadable lib, unwired reset hook) the switch was unreachable and a
deliberate release was filed as a BLIND fail-open — one contaminating row per
tool call for a whole session, in the numerator that log exists to protect.
ADR-001, README and the lib header all asserted the opposite. New
ge_release_or_failopen() asks the switch first and falls through to
gate_failopen otherwise; both arms exit. Measured after: gate off + no .turn
marker -> 1 escape row, 0 fail-open rows. Armed, same state -> unchanged.

A missing escape lib must not turn a fail-open into a fall-through, so each
gate defines a one-line fallback shim when the classifier is absent.

TESTS. `an unreadable escape lib leaves every gate ARMED` claimed three gates
and exercised two — removing the guard from enforce-frontmatter left all tests
green. Third leg added. Its scratch dir moves under TURN_STATE_DIR so teardown
reclaims it (a RETURN trap is unusable: bats runs with functrace, so it fires
on the first helper's return). Adds a $ROOT/README.md case and a degraded-path
classification test.

Suite 165 ok / 0 not ok. Both fixes falsified on a COPY: reverting either
reddens its test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ents

Final gate returned NO BLOCKING DEFECTS; these are its Should-Improves.

- The shim was entirely unpinned: deleting it (or its `shift`) survived all
  165 tests, while deleting it actually breaks the degraded path — an
  undefined function returns 127 and execution FALLS THROUGH into the deny, so
  a gate denies exactly where fail-open must release. Test 24 now covers it
  with the escape lib chmod 000; removing the shim reddens it. Test 22 could
  never catch this because it asserts the deny.
- Centralized the shim into gate-failopen.sh behind a `declare -F` guard
  instead of duplicating it in two hooks. Safe because gate-escape.sh is
  sourced first in both gates, so the real definition always wins; the guard
  only covers the state where that lib was unreadable.
- `ge_release_or_failopen` called `gate_failopen` unguarded, breaking its own
  never-returns contract in a state that exists in-tree: enforce-frontmatter
  sources gate-escape.sh but not gate-failopen.sh. Guarded.
- Both gate comments claimed the switch is evaluated "only once this gate knows
  it would otherwise have denied/blocked". False since round 2: on no-jq and
  lib-unreadable paths it is asked before audience and allowlist filtering, so
  a subagent call can write a row. The code cannot do better — without jq there
  is no audience to filter on — so the comments now say that, and say those
  rows are not audience-filtered.

Suite 165 ok / 0 not ok.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drewdrewthis
drewdrewthis marked this pull request as ready for review August 7, 2026 10:38
@drewdrewthis drewdrewthis self-assigned this Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/procedures/hooks/lib/gate-failopen.sh`:
- Around line 76-82: Add a literal “# PLUGIN ADAPTATION: <why>” comment
immediately before the fallback ge_release_or_failopen shim, identifying it as
plugin-specific behavior absent upstream. Preserve the existing guard and
function implementation unchanged.

In `@plugins/procedures/hooks/tests/gate-escape.bats`:
- Around line 267-268: Update the KEYS extraction in the hook-to-manifest
contract test to avoid grep’s non-portable -P option and work with BSD and GNU
toolchains. Preserve the existing behavior of extracting uppercase keys from
ge_enabled declarations across the hook scripts, sorting them, and validating
that the result is non-empty.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8b379afb-ae6c-46c7-9267-d5e4982572d0

📥 Commits

Reviewing files that changed from the base of the PR and between 1135439 and b710ca8.

📒 Files selected for processing (9)
  • README.md
  • docs/adrs/001-procedural-knowledge-system.md
  • plugins/procedures/.claude-plugin/plugin.json
  • plugins/procedures/hooks/am-i-done-gate.sh
  • plugins/procedures/hooks/enforce-frontmatter.sh
  • plugins/procedures/hooks/how-do-i-gate.sh
  • plugins/procedures/hooks/lib/gate-escape.sh
  • plugins/procedures/hooks/lib/gate-failopen.sh
  • plugins/procedures/hooks/tests/gate-escape.bats

Comment thread plugins/procedures/hooks/lib/gate-failopen.sh
Comment thread plugins/procedures/hooks/tests/gate-escape.bats Outdated
claude added 2 commits August 13, 2026 15:10
…extraction

Two findings from the first CodeRabbit pass (it skips drafts, so this is its
first read of this branch):

1. gate-failopen.sh — the ge_release_or_failopen shim is new behavior in a
   vendored file with no upstream counterpart, but carried only an explanatory
   comment, not the literal '# PLUGIN ADAPTATION:' marker the repo's vendoring
   rule requires. A future vendor sync could not tell it from upstream code.

2. gate-escape.bats — the manifest contract test extracted gate keys with
   grep -ohP. PCRE is a GNU extension; BSD grep rejects -P, KEYS comes back
   empty, and the non-empty guard then fails the test for a toolchain reason
   while reporting a contract breach.

Extraction output verified byte-identical to the PCRE form, and a mutation
probe (hook reads an undeclared gate key) still fails the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main moved 12 commits (0.2.0 -> 0.3.1) while this branch sat in review, so
the PR went DIRTY. Three conflicts, all resolved additively:

- plugin.json — took main's release-please-managed 0.3.1 and its expanded
  author block, kept the three userConfig booleans. Deliberately NOT
  hand-bumped to 0.4.0: main versions this file via release-please, so the
  feature bump belongs to the release commit, not to this branch.
- README.md — kept BOTH adaptation bullets (main's fork-skill model pin and
  this branch's configuration surface) and main's count-free "a further"
  wording, which no longer needs an ordinal now that the list grows.
- how-do-i-gate.sh — took main's newer deny-message wording ("File reads and
  read-only shell inspection stay available") and kept the switch guard above
  it. The rest of the escape-hatch wiring auto-merged.

Suite: 228 ok / 0 not ok on the merge result — up from 165, since main brought
its own tests along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to silence one gate of the procedures plugin without uninstalling it

2 participants