Skip to content

Hook-surface classification: 44 wired hooks classified policy vs behavioral (report-only) #2021

Description

@kyle-sexton

Context

Menu item #5 of the Boris-talk digest (knowledge-corpus, boris-cherny-we-cut-80-of-claude-code-s-qyPCVqFUyDo): hooks are officially carved out as the durable deterministic tier, BUT hooks that exist to correct model behavior (rather than enforce policy) are exactly what newer models obsolete. This is the classification pass that item called for — audit posture, report-only. Remediation is deliberately NOT included: the classification authority question (one-time audit PR vs standing playbook rubric — digest questions.md #5) is an operator decision this issue exists to inform.

Method: five parallel fresh-context read-only agents, one per plugin group, each reading hooks/hooks.json wiring first and every wired non-test script in full. Classification axes: mechanism (deny-gate / context-injection / deterministic-transform / notification-infra) and class (policy = invariant you'd keep with a perfect model; behavioral = corrects model behavior a better model gets right unaided; hybrid = both, with the split named).

Population note: the digest baseline said "108 hook scripts". Live count on main (19a3f7b3): 18 hook-shipping plugins, 66 non-test .sh hook scripts + 2 Python (disk-hygiene, wired from skills/clean/scripts/) + 2 project-local .claude/hooks scripts; 126 files under plugins/*/hooks/ including tests and sourced libs. The 108 figure appears to have counted test files; wired hook ENTRIES examined here: 44.

Roll-up

verdict count entries
Behavioral — clean ablation candidates 4 wirings / 3 scripts context-guard zone-crossing-inject.sh (wired twice: PostToolBatch + UserPromptSubmit); guardrails flag-commit-pr-skill-bypass.sh; guardrails workflow-resilience-check.sh
Hybrid — partial candidates (trim or narrow, don't delete) 8 context-guard zone-gate.sh (predicate is model-competence; deny path inert by default); autonomy lane-stop-gate.sh (lecture text behavioral, provenance hardening policy); guardrails hardcoded-path-check.sh; guardrails block-noncanonical-commit.sh (narrow to actual-newline -m); markdown-format (coaching strings only); typos-format (wrapper prose only); source-control pr-body-linkage-gate.sh + pr-linkage-mcp-gate.sh (latency mirrors of a required CI check that remains the authority)
Policy / infra — keep remaining ~32 all guardrails deny-gates (secrets, dangerous-git, no-verify, hook-bypass, convention), the 3 guardrails ground-truth verifiers (cli-flag / skill-reference / stale-path — behavioral purpose, non-derivable oracle), all 8 claude-ops observability emitters, all formatter transforms + their 2 embedded code-execution trust gates (markdown-format, powershell-format), worktree-create-gate, disk-hygiene engine gate + monitor, desktop-notification, rate-limit-guard recorder, session-start provisioning, post-compact-mark, observer-arm

Headline: in a default install, context-guard's zone-crossing-inject.sh is the ONLY always-on model-facing behavioral surface across the non-formatter plugins (autonomy's gate and the zone deny-path are default-off/advisory). It is also the subject of the already-filed #2009 (I23 flags its exit menu). Highest-value, lowest-risk ablation target; the two guardrails injectors (flag-commit-pr-skill-bypass, workflow-resilience-check) are the next two — static prose, no external oracle, kill-switchable without deletion.

Key nuance the policy/behavioral split surfaced: several hooks have a behavioral purpose but a policy-grade oracle — guardrails' cli-flag-verify (diffs written flags against the local binary's live --help), skill-reference-verify (globs the live plugin tree), stale-path-verify (git-history disappearance query). They correct hallucination, but with machine ground truth no model can know unaided. These are keeps; the report separates them from prose-only injectors explicitly so "it corrects the model" is not read as "delete it".

Per-plugin findings

guardrails (12 wired entries)

hook mechanism class ablation? rationale
secret-pattern-detection deny-gate policy no 12 credential regexes on write payloads; irreversible leak, model-independent
hardcoded-path-check deny-gate hybrid partial portability invariant, but only ever polices model sloppiness that review also catches
block-no-verify deny-gate policy no preserves the pre-commit/pre-push deterministic tier
block-dangerous-git deny-gate policy no irreversible-only block list (force-push, reset --hard, clean -f…)
block-hook-bypass deny-gate policy no keeps the Write/Edit gates non-optional (blocks Bash write routes)
flag-commit-pr-skill-bypass context-injection behavioral yes fixed prose nudge toward /pull-request create; no external truth consulted
block-noncanonical-commit deny-gate hybrid partial multi-line -m mangling is real, but the block at :769–789 denies ALL -m with no newline test — single-line git commit -m "fix: typo" blocked. Narrow to actual-newline and the residue is pure policy
block-convention-violation deny-gate policy no enforces the consumer repo's own declared subject pattern; opt-in
workflow-resilience-check context-injection behavioral yes two greps then a fixed ~120-word checklist; asserts nothing the model can't derive
cli-flag-verify context-injection hybrid no oracle = local binary's live --help; non-derivable ground truth
skill-reference-verify context-injection hybrid no oracle = live plugin tree state post-rename/delete
stale-path-verify context-injection hybrid no oracle = git-history disappearance; can't flag invented paths by construction

Notes: two structurally separate PreToolUse groups share the identical Bash|PowerShell matcher (could be one); three scripts carry stale "Triggered on Bash" headers while wired Bash|PowerShell.

context-guard / autonomy / session-flow (6 entries)

plugin hook mechanism class ablation?
context-guard zone-crossing-inject (PostToolBatch) context-injection (~1KB lecture + 4-option exit menu) behavioral yes — the #2009/I23 finding; payload hedges its own model-competence premise
context-guard zone-crossing-inject (UserPromptSubmit) context-injection behavioral yes — self-described redundancy cover for fresh-prompt turns
context-guard zone-gate deny-gate hybrid partial — deny predicate is "dumb context zone" model-competence guess; inert as shipped (advisory default); keep only the .compacted-marker branch
context-guard post-compact-mark infra transform policy no — records objective compaction fact; zero model-facing output
autonomy lane-stop-gate deny-gate + notify hybrid partial — block reason is a re-injected prompt admonition; trusted-config chain + operator alert are model-independent; default OFF
session-flow observer-arm infra policy no — out-of-band observer arming, anti-recursion guards; default OFF

Default-posture reframe: three of the four gates are inert at default config; zone-crossing-inject is the only always-on surface.

claude-ops (8 entries)

All eight are pure-observability telemetry emitters (StopFailure, ConfigChange, InstructionsLoaded, PermissionDenied, PreCompact, Skill-usage x2, PostToolUseFailure): policy class, zero behavioral rows, zero ablation candidates on the model axis. Separate dead-weight axis (do not conflate): six of eight are currently inert because no HOOK_TELEMETRY_SINK is wired anywhere in the repo — they exit at hook::telemetry_enabled || exit 0. The two skill-usage producers still write skill-usage.jsonl unconditionally. hook-telemetry-sink.sh is unwired but is the documented consumer end, not an orphan.

source-control / rate-limit-guard / desktop-notification / disk-hygiene / project-local (9 entries)

where hook mechanism class ablation?
source-control pr-body-linkage-gate deny-gate hybrid partial — self-described "POLICY gate, not a security guard"; motive is CI-round-trip latency; required CI check remains the authority; fails open on unreadable bodies
source-control pr-linkage-mcp-gate deny-gate hybrid partial — same split on the MCP surface
source-control worktree-create-gate transform/infra policy no — replaces creation to prevent nested-worktree rule-inheritance breakage; model not in the loop
rate-limit-guard record-rate-limit-stop infra (JSONL seam) neither n/a — StopFailure output is ignored by the harness; real question is whether anything still consumes stop-events.jsonl
desktop-notification desktop-notification notification neither n/a — output goes to the human, not the model
disk-hygiene destructive_guard.py (engine-gate) deny-gate policy no — enforces kill switch + data-root authority for the bundled engine only; hardens against harness fail-open (#1423)
disk-hygiene guard_launch_monitor.py context-injection neither (observability) no — detector for the PreToolUse fail-open shape (#1416); retire only if fixed upstream
project-local .claude/hooks/pr-linkage-mcp-gate deny-gate hybrid partial — distinct value is reach (cloud sessions with no plugin install); duplicate wiring note below
project-local .claude/hooks/session-start infra provisioning policy no — SHA-256-pinned installs, cloud-VM-only guard; genuine supply-chain gate

Duplicate double-fire: with source-control@melodic-software enabled, .claude/settings.json wires the repo-local mcp-gate on the byte-identical matcher as the plugin's — both fire, both exit 2 with near-identical stderr on every MCP PR create/update. Ablating either leaves the surface covered.

Second registration: disk-hygiene's SKILL.md frontmatter wires the SAME destructive_guard.py with no --mode → strict belt posture; the hooks.json engine-gate row alone understates its footprint.

Formatter family (9 entries: actionlint, bash-format, biome-format, eol-normalizer, go-format, markdown-format, powershell-format, ruff-format, typos-format)

All PostToolUse Write|Edit, all exit 0 unconditionally, zero deny-gates against the model. Seven pure policy (config-gated deterministic transforms + external-tool ground-truth relays). Two hybrids: markdown-format (partial — ablatable surface is only the conduct-coaching strings, not the transform, the finding relay, or its fail-closed code-execution trust gate on .cjs/.mjs configs); typos-format (partial — report-only by default, so the wired surface is mostly injected wrapper prose; finding list is policy). powershell-format keeps: its CustomRulePath trust gate is a real consent invariant.

Cross-cutting: hook-utils.sh is one 72,723-byte file duplicated identically across 16 plugins (SSOT lib/hook-utils.sh, CI-checked); any shared-helper change hits all 16. Every plugin hook has a <name>_enabled kill switch — ablation can be tested by config-disable, no deletion required.

Proposed work (gated on the authority decision)

Acceptance criteria

References

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: mediumReal value, no hard deadline; normal backlog flow.status: needs-decisionAwaiting a human or maintainer judgment call.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions