Skip to content

perf(guardrails): cut the benign-path spawns inside the dispatcher's guards - #3676

Merged
kyle-sexton merged 18 commits into
mainfrom
perf/guard-hot-path
Sep 2, 2026
Merged

kyle-sexton merged 18 commits into
mainfrom
perf/guard-hot-path

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No related issue: phase 4a of the hook performance program tracked in #3623; that tracking issue stays open until phase 8. Stacked on PR #3621 (its base); retarget to main once #3621 merges.

Summary

The guardrails dispatcher runs eight Bash guards per Bash call and three verifier guards per Write. Profiled on this Windows host, skill-reference-verify.sh built a plugin index with two jq per manifest (about 150 jq spawns, 11.4 s) before knowing whether the written content cited any skill, block-convention-violation.sh forked the convention resolver twice per Bash call, and several guards spent dirname, basename, git rev-parse and jq on paths the dispatcher had already primed. This PR removes those spawns with every decision byte-identical.

Fix

  • skill-reference-verify.sh: the plugin index is built only after the content is known to reference a skill; on a benign in-repo Markdown write the PostToolUse dispatch drops from 155 jq execs and 151 manifest reads to 7 and 1.
  • block-convention-violation.sh: the two resolved patterns are cached per repo root in plugin data, keyed on the convention file's path and mtime, so the resolver forks once per convention change; contract cases prove the cached pattern equals the forked form and that an mtime change re-forks.
  • Benign-path spawn cuts across the Write guards and verifiers (parameter expansion for dirname/basename, primed fields instead of re-run jq/git).
  • Version 0.31.1 with a CHANGELOG entry and a dated README accounting section (two paired passes, spawn-equivalents with the floor stated; the first pass's PreToolUse Write credit is retracted in place, since that path did not move).

Verification

  • 12 interleaved trials, base versus head, telemetry sink unset: PostToolUse:Write on an in-repo .md 18200 to 3841 ms (389.7 to 82.2 spawn-equivalents); PreToolUse:Bash benign 2494 to 2042 ms (55.8 to 45.7). With the sink set: 19435 to 5146 ms and 3969 to 3467 ms.
  • Decision equivalence against the base on nine payloads (benign Bash, $(), two MUST-fire git payloads, git commit, in-repo PostToolUse and PreToolUse Write, a secret-bearing Write, a synthetic-repo convention violation and a conforming subject): stdout, stderr and exit code identical, cold and warm.
  • Suites: 15 of 17 green; the two failing on this Windows host (block-windows-drive-tmp.test.sh, hardcoded-path-check.test.sh) fail the identical case sets on the base.
  • A fresh-context verifier passed all eight criteria, re-tracing the dispatch itself (base 155 jq execs, head 7).
  • check-changelog-parity.sh --check and --check-preserved exit 0; shellcheck and markdownlint clean; no em dashes in added lines (byte check).

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP

kyle-sexton and others added 14 commits September 1, 2026 23:00
…f-gate the path-specific hooks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…DMEs and changelogs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…ts, shellcheck info findings)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
A pattern beyond a bare command name still spawns the process on a command
containing a substitution; the harness reports it as RAN(best-effort).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
Main re-bumped seven of this branch's ten plugins to the very numbers the
branch had already claimed, and moved source-control and disk-hygiene past
them, so the branch could not merge and its versions were ambiguous.

Conflict resolution keeps main's content and re-lands this branch's changes
on top:

- Ten plugin manifests take main's file wholesale (main re-encoded the em
  dash in several descriptions as \u2014) and carry a version one patch
  above main's: ruff-format 0.6.29, bash-format 0.7.29, biome-format
  0.6.28, go-format 0.3.31, powershell-format 0.7.31, actionlint 0.8.28,
  instruction-placement 0.11.19 (0.11.18 is claimed by another open
  branch), source-control 0.55.40, disk-hygiene 0.21.3. guardrails keeps
  0.31.0, still above main's 0.30.4.
- Ten changelogs carry this branch's entry retitled to that number and
  placed above every entry main added, with main's entries intact.

actionlint, bash-format and instruction-placement manifests auto-merged at
the colliding number and are re-bumped here explicitly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…reference exists

skill-reference-verify built its plugin name -> directory index from every
plugin manifest before it had looked at whether the written content cites a
skill at all. Two jq processes per manifest, 76 manifests in this marketplace:
152 jq plus 149 tr spawns on a PostToolUse:Write of a file with no references,
measured at 11,430 ms (272 spawn-equivalents against a 42 ms spawn floor) on
Windows Git Bash. That was the entire PostToolUse:Write cost of this plugin.

Nothing below the reference scan reads the index, and the scan finds its
candidates with no plugin knowledge, so the build moves into a function called
once, only when the scan produced a candidate. The plugins-root and manifest
gates stay exactly where they were: their exit paths deliberately skip
telemetry, and moving them would change that.

The repo-root anchor also stops forking: ${FILE%/*} with a slashless fallback
replaces $(dirname "$FILE"), one fewer subshell per Write and Edit.

Tests count the manifest reads through a jq shim rather than asserting the
source text: zero manifest reads for a write that cites nothing, a non-zero
count when it cites something, and exactly two per manifest when it cites two
references, which is what proves the index is built once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
block-convention-violation forked the convention resolver twice on every Bash
and PowerShell tool call, because the resolver answers one key per call. On
Windows Git Bash that measured 423 ms, 10.1 spawn-equivalents against a 42 ms
spawn floor, paid on every command the agent runs, to re-derive an answer that
changes only when the convention files change.

The resolved pair is now cached per repo root under the plugin data directory
and invalidated by mtime against every file the resolver reads: the team
markdown, the well-known neutral YAML, and an explicit convention_source target
when one is declared. Freshness is the `-nt` builtin rather than a stat
process, so a warm run spawns nothing at all. The resolver itself is untouched
and stays the only authority for what a pattern is; only its answer is stored.

Equal mtimes read as not-newer, so a same-tick write re-resolves instead of
serving a stale pattern. The entry records the root it was resolved for and
ends with a terminator line, so neither a filename collision between two roots
nor a truncated write can quietly disable the gate. With no plugin data
directory the gate simply does not cache.

Known residual, recorded in the file: the resolver's well-known rung honours
its YAML only when git reports it tracked, and tracked status can change with
no mtime change. Such a change is picked up when a dependency is next written.
Probing it would cost the git spawn this cache exists to remove.

Tests compare the cached patterns against the forked patterns value by value,
not decision by decision, count resolver forks through a shim to prove a warm
run makes none, and drive a real convention change through touch -d so the
invalidation is a genuine mtime move rather than two writes inside one tick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…es on ruled-out work

Three more always-on guards did expensive setup before checking whether the
payload could ever produce a finding.

stale-path-verify listed the entire git index on every Write and Edit. A write
that cites no inline-code token has no candidate for that list to answer about,
so the warm now happens only when the token scan produced one. It still happens
in the same shell and still precedes the candidate loop, which is what the
subshell-assignment fix it carries requires.

cli-flag-verify ran its fragment pipeline, three processes, on content that
cannot contain a flag. Every candidate it can produce is a (bin, chain, flag)
triple and a flag token always carries a dash, so content with no dash anywhere
is now skipped ahead of the scan. The Edit reconstruction takes the same gate,
since its anchor is a flag token from the hunk.

secret-pattern-detection, cli-flag-verify and stale-path-verify each resolved
the repository root through a $(dirname ...) subshell, a fork per call on
Windows Git Bash, and now use parameter expansion with a slashless fallback.

Nothing that is reported changes: the skipped work is the scan, never the
report, and telemetry and the exit paths are reached exactly as before. Version
bumped to 0.31.1 with the accounting recorded in the CHANGELOG.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
t.jsonl is the transcript_path a Phase 4a profiling payload names. It is
measurement scaffolding and was never meant to be tracked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
… residual

The README's budget accounting gains the 0.31.1 measurement beside 0.31.0's.
0.31.0 cut the number of hook processes; this cut what each guard spends inside
one, and the per-guard slice table states both.

The method paragraph says why the two trees are measured paired and alternating
rather than one after the other: the host is shared and its spawn floor moved
from 42 ms to over 100 ms while the work was done, so a sequential comparison
would have booked that drift as the change. It also records that the Write
sample names a file inside the repository, because every Write and verifier
guard early-exits on an outside path and an out-of-tree sample measures a no-op.

The residual is stated with numbers rather than left implied. PreToolUse Bash is
still 52.6 spawn-equivalents against the fleet target of 8, and nearly all of
what remains is telemetry inside the synced hook library, one subshell fork per
guard, and the classification each guard exists to do. Three further cuts were
measured and deliberately not made; each is named with what it would have
returned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…ale back-reference

Four corrections, all to prose that describes the 0.31.1 measurement rather than
to any behaviour.

The manifest count was 76 in three places. The glob the index actually walks is
`plugins/*/.claude-plugin/plugin.json`, which returns 74 on this tree and on the
base; the ~150 index spawns the same comment states are 2 times 74, so the count
was the figure that drifted, not the spawn total.

The CHANGELOG cited 339.8 falling to 21.6 spawn-equivalents for the PostToolUse
Write path. 339.8 comes from the superseded standalone before run, whose per-trial
cold plugin-data directory measured every cache cold, and 21.6 appears in no
recorded run at all. The README already states that the paired columns are the
comparison of record, so the CHANGELOG now quotes them: 368.4 to 79.3, and 12.8
to 3.0 for the convention gate in place of a before-only 10.1.

The CHANGELOG said three guards dropped the `$(dirname ...)` subshell. Four did:
cli-flag-verify, secret-pattern-detection, skill-reference-verify and
stale-path-verify.

One added comment in stale-path-verify cited an issue number a future reader of
the file cannot see. The sentence above it already carries that reference from
before this branch; the new sentence now names the fix instead of the ticket.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…olUse Write gain

The 0.31.1 accounting table credited the PreToolUse Write path with 10.1
spawn-equivalents. A second paired pass, run on a much quieter host with the
spawn floor back at 42 to 47 ms rather than 85.5 and 101, does not find it: 12
interleaved trials per tree put that delta at -0.9 with the telemetry sink unset,
-3.2 with it set, and -1.9 with it set and CLAUDE_PROJECT_DIR unset.

The row was drift rather than a result. Two of the three guards on that path are
untouched by this phase and moved 1.1 and 0.9 in the first pass, the same size.
And the single line the phase changed in secret-pattern-detection sits inside
emit_tel, on the branch taken only when CLAUDE_PROJECT_DIR is empty, so a session
that sets it never executes the change at all; the unset row was measured for
exactly that reason and shows no gain either.

The other two rows do reproduce, and the paragraph now states the reproduction
figures beside the originals.

The telemetry sentence said the sink-unset Bash path measured roughly half its
cost. Measured, it is 45.7 spawn-equivalents against 79.5, so telemetry is about
two fifths of the wall and 4.2 per guard across the eight.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T19:54:16.904928Z baacee5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 7m 18s —— View job


Claude code review

  • Load /review:code-review skill instructions (the Skill tool errored on invocation; read plugins/review/skills/code-review/SKILL.md directly and followed it manually)
  • Gather context: PR diff against origin/perf/hook-fanout-consolidation, changed files, CLAUDE.md/AGENTS.md conventions
  • Apply skip gate — PR is open, non-draft, and the change is substantive (463/-26 across 11 files, real logic in 5 hook scripts) — proceeded with full review
  • Review changed hook scripts and tests for correctness, error handling, maintainability
  • Verify claimed decision-equivalence and spawn-reduction logic by reading the diffs directly
  • Post inline findings and summary

This is a well-scoped, well-tested perf change: four guards now skip expensive setup (plugin-manifest indexing, git ls-files, the flag-fragment pipeline) until their own cheap gate proves there's something to adjudicate, and block-convention-violation.sh adds a repo-root-keyed, mtime-invalidated cache around the convention-pattern resolver. I traced the caching/invalidation logic (freshness via -nt, the terminator-line truncation guard, the sanitized-root-collision handling, the convention_source pointer re-parse) and the lazy-build ordering in skill-reference-verify.sh/stale-path-verify.sh (index/tracked-file-cache builds strictly precede their only use sites, gated on the same emptiness check that would make the work moot anyway) — both hold up.

Two findings posted inline, both narrow:

  1. plugins/guardrails/hooks/cli-flag-verify.sh:105-107 (pattern repeats identically in secret-pattern-detection.sh, skill-reference-verify.sh, stale-path-verify.sh) — the new FILE_DIR="${FILE%/*}" replacement for dirname produces "" rather than "/" for a root-level absolute path (/bar.md), and because hook::repo_root's ${1:-.} treats an empty argument as unset, this silently falls through to resolving against the process CWD instead of failing unresolved the way the old dirname-based code did. Very unlikely to be reachable in practice (a repo file's file_path essentially never has zero intermediate directory components), but it's a real, systematic (4x) divergence from the PR's "byte-identical" claim, not just a one-off. permalink
  2. plugins/guardrails/hooks/cli-flag-verify.sh:290-300 — the new HAS_FLAG_SHAPE pre-gate has no accompanying test, unlike the equivalent optimizations in the other three guards (each of which got a dedicated spawn-count or source-assertion test in this same PR). cli-flag-verify.test.sh wasn't touched. Low real-world impact since most content contains a literal - somewhere, but it's a genuine coverage asymmetry against the PR's own "every decision is unchanged, proven by contract tests" bar.

Everything else — the block-convention-violation.sh cache's freshness/invalidation semantics, the lazy plugin-index and tracked-file-cache builds, the version bump and CHANGELOG entry — checked out correctly against the source. I did not independently re-run the shellcheck/markdownlint/em-dash checks or the author's Windows-host timing figures; those are author-claimed and unverified by me.

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 23s —— View job


Security review complete

  • Gather context (PR body, changed files list, custom instructions)
  • Fetch full diff against origin/perf/hook-fanout-consolidation
  • Review block-convention-violation.sh caching logic for injection/race issues
  • Review skill-reference-verify.sh lazy-index changes
  • Review cli-flag-verify.sh, secret-pattern-detection.sh, stale-path-verify.sh spawn-reduction edits
  • Post findings

Scope: diff origin/perf/hook-fanout-consolidation...HEAD (11 files, all under plugins/guardrails/).

No blocking security findings. This is a spawn-reduction refactor of existing guard logic; none of the changes cross a new trust boundary, and the new caching layer is scoped to already-trusted local paths.

What I checked

  • dirname/basename → parameter expansion (cli-flag-verify.sh, secret-pattern-detection.sh, stale-path-verify.sh): ${FILE%/*} with a [[ "$FILE_DIR" == "$FILE" ]] && FILE_DIR="." fallback reproduces dirname for the paths these hooks actually receive (tool_input.file_path). No shell-injection surface — the value is used only in parameter expansion and passed to git -C, never interpolated into a command string or eval.
  • Convention-pattern cache (block-convention-violation.sh#L91-L210):
    • Cache key is REPO_ROOT sanitized with ${REPO_ROOT//[^A-Za-z0-9]/_} before use as a filename component — no path traversal into $CLAUDE_PLUGIN_DATA.
    • A collision between two roots that sanitize to the same string can't leak one repo's pattern into another's: the cache entry records the un-sanitized REPO_ROOT and is only trusted when it matches exactly (covered by the "entry records its repo root" test case).
    • Invalidation is mtime-based ([[ "$CONV_CACHE" -nt "$conv_dep" ]], line 168) and a truncated/corrupt entry (missing END terminator) is treated as a miss, not a silent pass-through — block-convention-violation.test.sh cases 3/4/6 exercise warm-hit, mtime-bump invalidation, and truncation, and all fail toward re-resolving (i.e., toward stricter enforcement), never toward disabling the gate.
    • The write path (lines 203-206) writes to $CONV_CACHE.$$ then mv -fs into place — standard temp-then-rename, consistent with this file's existing atomic-write style elsewhere in the plugin. $CLAUDE_PLUGIN_DATA is harness-supplied, not attacker-influenced, so this isn't a new trust-boundary crossing versus the rest of hook-utils.sh (e.g. the pre-existing marker-file writes under the same directory).
    • One pre-existing-shape edge case, not a regression: if a convention dependency file is deleted (rather than edited), -nt treats a nonexistent file as "older," so the cache stays warm and keeps enforcing the last-known pattern until some other dependency's mtime changes. That fails toward more enforcement (blocking commits under a retired convention), not toward a bypass, so it's not a security concern — flagging only as a correctness note if the author wants it, not something this review is asking to be fixed.
  • skill-reference-verify.sh lazy plugin-index build: gated on ((${#REFS[@]})) && build_plugin_index, i.e. the index (and the jq reads of every plugin manifest) is skipped only when there are zero extracted skill references to check against it — the skip path has nothing left to decide, so it can't produce a false negative.
  • Confirmed resolve-convention-pattern.sh (the actual pattern authority) is untouched by this PR — the cache only memoizes its output, never reimplements its logic.

No secrets, credentials, or unsafe eval/source-of-untrusted-data patterns found in the diff. README.md/CHANGELOG.md/plugin.json changes are documentation/version-only.

kyle-sexton added a commit that referenced this pull request Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Last security-reviewed head: e2a9f06a62783f246a41d1a521270254cdf4c4d4. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: baacee5d97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-convention-violation.sh Outdated
Comment thread plugins/guardrails/hooks/cli-flag-verify.sh
Comment thread plugins/guardrails/hooks/cli-flag-verify.sh
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

kyle-sexton and others added 2 commits September 2, 2026 16:11
…t plugins

Second merge-forward. Since the last one, main claimed the exact version
numbers this branch had bumped to in eight plugins (actionlint 0.8.28,
bash-format 0.7.29, biome-format 0.6.28, disk-hygiene 0.21.3, go-format
0.3.31, powershell-format 0.7.31, ruff-format 0.6.29, source-control
0.55.40), so `check-changelog-parity.sh --check-bump origin/main` failed
and a plugin cache pointed at the branch reported "already at the latest
version".

Each of those eight is rebumped one patch above main's value and its
CHANGELOG entry retitled to match; main's entry keeps its own heading and
text verbatim below. guardrails (0.31.0) and instruction-placement
(0.11.19) were still above main and are unchanged.

The dispatcher and the `if` filters in all ten hooks.json files are
byte-identical to the pre-merge head.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
Merge origin/perf/hook-fanout-consolidation (cd5aefb) into
perf/guard-hot-path. The merge was clean and needed no resolution: the
base landed the guardrails dispatcher as 0.31.0 and this branch's 0.31.1
entry already sits above it in plugins/guardrails/CHANGELOG.md, so the
manifest version line and the changelog top region both merged without a
conflict.

Verified after the merge: scripts/check-changelog-parity.sh exits 0 for
both --check-bump origin/main and --check, no conflict markers remain in
the tree, and the two guardrails hook suites pass
(run-guards.test.sh 54/54, skill-reference-verify.test.sh 120/120).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 17s —— View job


Claude code review

  • Load /review:code-review skill instructions
  • Gather context: PR diff against origin/perf/hook-fanout-consolidation, changed files, CLAUDE.md/AGENTS.md conventions
  • Apply skip gate
  • Confirm no re-review needed

Skip gate hit: condition 4 (this head already has a successful review from this lane that still applies).

The head moved from baacee5 to 6046f44874f86a3dda22e467f7ae0db40dc70cf7 via chore(merge): bring perf/guard-hot-path current with its base — a merge of origin/main into the base branch, then a merge of the base into this branch. I diffed baacee5..HEAD and confirmed:

  • plugins/guardrails/hooks/*.sh and *.test.sh (the actual reviewed logic) are byte-identical to baacee5git diff baacee5..HEAD -- plugins/guardrails/hooks/ is empty.
  • The PR's own diff against its base (git diff origin/perf/hook-fanout-consolidation...HEAD) is unchanged: same 11 files, same +463/-26.
  • plugins/guardrails/CHANGELOG.md and README.md picked up one unrelated upstream entry ([0.30.5], an options-reference wording tweak carried in from main) — not part of this PR's own changes.

This lane already completed a full review of this exact code at baacee5 (see the "Claude finished @kyle-sexton's task" comment above, plus its two inline findings on cli-flag-verify.sh:107 and cli-flag-verify.sh:300). Since no reviewable line changed, that review still applies and I'm not re-posting or duplicating findings per the skip gate.

No new inline comments from this run.

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 45s —— View job


Claude security review

  • Gather context (PR body, changed files, custom instructions)
  • Invoke /review:security-review skill and follow it exactly
  • Post findings

Skip gate hit: this head's security review already applies.

The commits since the last security-reviewed head (baacee5d97eb6ffe33b5aa6c67b5da3d1d3e3aa4, reviewed here) are cd5aefb and 6046f44 — a merge from origin/main and a rebase-current merge. Diffed against baacee5, they touch only plugins/guardrails/CHANGELOG.md and plugins/guardrails/README.md, and only prose (a changelog entry for an unrelated 0.30.5 doc fix, and a README paragraph pointing at the plugin-reconfiguration convention doc instead of restating a verified-version note). No hook script, workflow file, or other security-relevant surface changed.

Per /review:security-review's skip gate (item 3: "this head already has a successful security review that still applies"), no new review is needed — the prior review of baacee5 (which covered all 11 changed files, including the caching logic in block-convention-violation.sh and the spawn-reduction edits across the four Write guards) still stands. No new findings.

@kyle-sexton
kyle-sexton changed the base branch from perf/hook-fanout-consolidation to main September 2, 2026 22:29
kyle-sexton and others added 2 commits September 2, 2026 19:14
…nce and anchor root-level paths as dirname did

Three review findings on #3676.

The convention-pattern cache tested freshness with `cache -nt dep` alone,
which is true against a missing file, so an entry warmed while the team
markdown, the well-known YAML, or an explicit `convention_source` target
existed kept enforcing a policy the team had since deleted, where the
resolver answers no enforcement. The entry now records each dependency
with its existence at warm time, and a recorded-as-present file that is
missing, or a recorded-as-absent file that now exists (even with an
older mtime), reads as a miss. The warm path still forks the resolver
zero times; both transitions are suite cases that fail against the
previous hook.

The parameter expansion that replaced the `dirname` fork in four guards
produced an empty string for a root-level `/file.md`, which
hook::repo_root read as `.` (the process CWD) rather than `/`. The
empty case now yields `/`. secret-pattern-detection proves it end to
end through a `git` shim recording the `-C` anchor; the three
PostToolUse guards cannot reach that shape end to end (the file must
exist and `/` is not writable), so their suites lift the seam from the
hook source and hold it to dirname's answer for each path shape.

cli-flag-verify's flag-shape pre-gate now has contract coverage: a
`sed` shim proves no-dash content spawns no scan process on both the
Write and the bare-bin Edit path, and dash-bearing content is held to
the identical UNKNOWN_FLAG line and count the suite already pinned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011boRpMmkKhritwmR4ZPMvP
…landed

Every conflicted file outside guardrails takes main's text; guardrails keeps
its 0.31.1 entry above main's 0.31.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton enabled auto-merge (squash) September 2, 2026 23:23
@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 18s —— View job


Claude code review

  • Load /review:code-review skill instructions
  • Gather context: PR diff against origin/main, changed files, CLAUDE.md/AGENTS.md conventions
  • Apply skip gate
  • Review the fix commit (ff9347004) against the three prior findings
  • Post findings

Skip gate: partially applies. Since the last review of this lane (head 6046f44), two commits landed: ff9347004 (a fix commit addressing this lane's own prior findings plus Codex's P1) and e2a9f06 (a merge bringing the branch current with main after base PR #3621 landed — confirmed via git diff ff9347004..e2a9f06 -- plugins/guardrails/, which only touches unrelated run-guards.sh/dispatcher content carried in from main, not this PR's own diff). So this is a genuine re-review of ff9347004, not a repeat.

I traced all three fixes against the findings they claim to close:

  1. Convention-cache existence tracking (block-convention-violation.sh, Codex P1). The cache entry now carries one DEP <0|1> <path> line per dependency ahead of END. On read, each line is checked in order against CONV_DEPS: an existing file must be recorded present and have an unchanged mtime; a missing file must be recorded absent. A shape mismatch (wrong path at that index, or a different dependency count — e.g. an added/removed convention_source pointer) breaks the read as a miss. I worked through the disappear case (recorded 1, now missing → else-branch requires flag 0 → miss) and the reappear-with-old-mtime case (recorded 0, now exists → requires flag 1 → miss) by hand against the code at block-convention-violation.sh:168-210 — both correctly invalidate. Suite cases 7 and 8 exercise exactly these transitions end to end (delete the team file → enforcement drops; a higher-precedence YAML appears with a mtime older than the cache → its pattern takes over), and the PR notes both fail against the pre-fix hook.
  2. Root-level path anchor (cli-flag-verify.sh, secret-pattern-detection.sh, skill-reference-verify.sh, stale-path-verify.sh). All four now add [[ -n "$FILE_DIR" ]] || FILE_DIR=/ right after the existing no-slash guard, so /bar.md anchors on / instead of falling through to hook::repo_root's CWD default — closing the exact divergence flagged. secret-pattern-detection.test.sh proves it end to end through a git -C-argument-logging shim; the three PostToolUse guards (which can't reach a root-level file_path end to end, since hook::read_file_path requires the file to exist and / isn't writable) instead lift the FILE_DIR assignment lines verbatim out of the hook source via sed and eval them against /bar.md, bar.md, and /a/b/bar.md, asserting parity with real dirname. That's a reasonable way to cover an otherwise-unreachable shape.
  3. HAS_FLAG_SHAPE gate coverage (cli-flag-verify.sh). New "FLAG-SHAPE PRE-GATE" section in cli-flag-verify.test.sh shims sed to prove zero scan-process spawns for no-dash content on both the Write and bare-bin Edit paths, and confirms dash-bearing content still produces the exact UNKNOWN_FLAG line/count the suite pinned before the gate existed (with a positive sed count asserted, so the "still runs" side isn't just an artifact of a no-op gate).

All three fixes read as correct and directly test the failure mode each finding described; I didn't find a new gap they introduce. The CHANGELOG got three matching entries under the existing unreleased [0.31.1] block (no version bump needed, consistent with them landing before release).

Not independently verified by me: I don't have Bash execution available in this run (test-script invocations were blocked pending approval), so I read the suite additions rather than running block-convention-violation.test.sh / cli-flag-verify.test.sh / the others myself. The author's stated pass results, the Windows-host timing figures, and the shellcheck/markdownlint/em-dash checks remain author-claimed and unverified here.

No new findings to report.

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 1s —— View job


Claude security review

  • Gather context (PR body, changed files, custom instructions)
  • Invoke /review:security-review skill and follow it exactly
  • Post findings

Skip gate not applicable — reviewed the delta since the last security-reviewed head.

Last security-reviewed head was 6046f44 (see prior comment). Since then, 1abc1b6 (unrelated PR #3675, formatter hooks) and ff93470 landed, followed by merge commit e2a9f06. ff93470 is a real, security-relevant fix commit — not cosmetic — so this lane's condition-3 skip does not apply, and I reviewed it directly rather than the whole PR again (the other 11 files are unchanged since the last full review at baacee5).

ff93470 fixes exactly the three findings raised on this PR (Codex's P1 on block-convention-violation.sh, and my two inline findings on the dirname-replacement divergence and the missing flag-shape test). I re-traced each fix against its diff:

  1. Convention-cache existence tracking (block-convention-violation.sh#L168-L226): the cache entry now records a DEP <0|1> <path> line per dependency, sampled before the resolver forks. On read, a recorded-present dependency that's now missing, or a recorded-absent one that now exists (even with an older mtime — the archive-restore case -nt alone can't see), is a miss. Malformed/out-of-order/short entries (dep-count mismatch, missing END) all fail toward conv_fresh=0, i.e. toward re-resolving/enforcing, never toward a silent bypass. This closes the "deleted policy stays enforced-or-not-enforced against stale cache" gap correctly, in both directions, and the new suite cases 7 and 8 demonstrate both transitions failing against the pre-fix hook.
  2. Root-level path anchor (cli-flag-verify.sh, secret-pattern-detection.sh, skill-reference-verify.sh, stale-path-verify.sh): [[ -n "$FILE_DIR" ]] || FILE_DIR=/ now catches the case the old [[ "$FILE_DIR" == "$FILE" ]] guard missed, restoring dirname's / answer for a root-level /file.md instead of the empty string hook::repo_root's ${1:-.} was silently reading as .. Applied identically across all four guards. secret-pattern-detection.test.sh proves it end-to-end via a git -C argument-recording shim (/secrets.env now anchors on /, not . or CWD).
  3. cli-flag-verify.sh flag-shape pre-gate coverage: new sed-shim-based tests prove the gate actually skips extract_candidates/reconstruct_partial_edit for no-dash content on both Write and Edit paths (0 spawns), and that dash-bearing content still resolves to the identical UNKNOWN_FLAG line/count as before the gate existed.

No new security-relevant surface was introduced by this fix — it's a targeted correctness/enforcement-integrity patch to logic already in scope. I did not find any new issue in this delta. I wasn't able to execute the shell test suites in this sandbox (Bash tool required interactive approval that wasn't available), so this is a static trace of the diff, not a suite run — the diff-level reasoning holds regardless, and the added test cases described above are structured to fail against the pre-fix code if the logic were wrong.

The other 10 files (README/CHANGELOG/version bump in the merge) carry only documentation and unrelated upstream content pulled in by e2a9f06; no action needed there.
· branch perf/guard-hot-path

@kyle-sexton
kyle-sexton merged commit 9f07fb5 into main Sep 2, 2026
68 checks passed
@kyle-sexton
kyle-sexton deleted the perf/guard-hot-path branch September 2, 2026 23:34
kyle-sexton added a commit that referenced this pull request Sep 2, 2026
… main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Sep 3, 2026
No related issue: phase 8 of the hook-performance program, tracking
issue #3623

## Summary

The hook-performance program landed nine PRs today (#3621, #3662, #3666,
#3668, #3671, #3675, #3676, #3677, #3678, plus dotfiles #620). This PR
records the outcome where the convention says it lives: the hook-budget
convention gains a dated reference-figures section in spawn-equivalents
with the harness sha256, and the four plugins that lacked a hook budget
accounting row gain one under Requirements. Documentation only; no hook,
script or registration changes.

## Fix

- `docs/conventions/hook-budget/README.md`: a "Reference figures
(2026-09-02, after the hook-performance program)" section with the
harness identity, the before-and-after spawn-equivalents per surface
(before at S = 33 ms, after at S = 18 ms on `main` `5e3d749cb`), the
reference-host conversion, and a plain reading against the budget table:
per-turn rows meet the 500 ms ceiling; per-tool-call rows sit at 1.4 to
1.9 s against the 1 s typical ceiling, with the guardrails dispatcher
named as the whole of the remainder.
- `plugins/typos-format/README.md`, `plugins/eol-normalizer/README.md`,
`plugins/markdown-format/README.md`, `plugins/context-guard/README.md`:
a "Hook budget accounting" section each, carrying the measured rows
their CHANGELOG entries already state (36.3 to 26.0, 41.0 to 21.5, 41.6
to 32.0 spawn-equivalents; 11 to 2, 9 to 4 and 6 to 1 processes for
context-guard), what changed, and the residual. guardrails and
rate-limit-guard already carried one.
- Version bumps with a documentation-only CHANGELOG entry: typos-format
0.6.37, eol-normalizer 0.6.30, markdown-format 0.11.40, context-guard
0.7.36.

## Verification

- Final harness run on the installed cache at `main` `5e3d749cb`,
`--runs 3`, S = 18 ms, valid, quiet host; every measured plugin's cache
directory byte-compared against `origin/main` (0 stale files in 17);
`enabledPlugins` unchanged against the pre-program snapshot; 52
`hooks.json` entries listed, every one `type: command`, no `async` row.
The per-event block, the STATED CHECK and the reading against goal (B)
are in the program's PLAN.md and DEVIATIONS.md on
`perf/hook-performance-program`.
- Per event (ms, slowest hook): PreToolUse:Bash 2,475 before to 1,599
after; PostToolBatch 1,254 to 282; UserPromptSubmit 975 to 297; in-repo
PostToolUse:Write 13,225 to 1,949; in-repo PostToolUse:Edit 17,192 to
3,048.
- `markdownlint-cli2` 0 issues on the five files; em dash counts
unchanged; `scripts/check-changelog-parity.sh --check-bump origin/main`
exit 0; `scripts/affected-tests.sh --explain` selects no suites (every
changed file is a recorded no-suite class covered by a non-shell CI
lane).

## Related

- #3623 (tracking issue)
- Merged today: #3621, #3662, #3666, #3668, #3671, #3675, #3676, #3677,
#3678; dotfiles #620

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <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.

1 participant