Skip to content

Claude Code hooks - Field report from one session with suggested tunes #10654

Description

@andreasjordan

The .claude/hooks/ gates have been running for a while now, so here is a field report: every hook firing from one long, productive session (2026-08-29) was checked against the current hook sources. The architecture held up well — fail-open, budget-bounded, portable, and two gates caught real problems. Six observations follow, each with a verdict and a concrete suggestion. They are suggestions only — the hooks are yours, @potatoqualitee.

Hook Fired True positives Verdict
stop-no-deflection.sh 2× 0 of 2 tune
stop-todo-report.sh 3× 1 of 2 lines tune
stop-verify.sh 2× — fix bug
validate-style.ps1 3× blocked 0 of 3 tune
pre-edit-read-check.sh 1× by design leave
Remove-Item guard 3× — not a repo hook

stop-no-deflection.sh — tune

Fired twice, both false positives, no true positive all session. It flagged "separate PR" and "pre-existing" inside factually accurate reports — the words described real PR topology and real line ages, not blame-dodging. Each firing cost a full response rewrite.

The pattern list mixes two kinds of phrases. Some are genuinely evasive (not my responsibility, I did not introduce). Others are ordinary repo vocabulary: this project's policy is literally one PR per command, so "separate PR" appears in almost every honest status report, and "pre-existing" is standard triage language when a full-suite failure predates the branch.

Suggestion: drop the fact-shaped patterns — the pre-existing family, existed before, was already (broken|failing|there), already existed, not introduced by, and separate (refactor|effort|task|ticket|issue|PR) — and keep the evasive ones. Alternative with zero risk of losing catches: route it through emit_system_message (advisory) instead of a block.

stop-todo-report.sh — tune

Fired three times (the full stop-guard budget) on the same two lines. One was a true positive in spirit: a TODO: in a test file that correctly pushed a fixture rebuild up the priority list — that work then happened the same session. The other was the word "workaround" inside .EXAMPLE help prose about a SQL 2000 engine limitation — correct documentation, not a marker.

The root cause: the hook greps the entire changed file (line 22), not the lines the session added. dbatools deliberately keeps "workaround" comments documenting SMO defects — six of them recently became upstream issues — so any edit to such a file flags forever. The observed failure mode is worse than noise: prose has already been reworded (an XXX doc changed to XXXX) to dodge the grep, which is the repo adapting to the hook instead of the hook to the repo.

Suggestion: two independent tunes, either alone helps. (1) Scan only added lines — git diff -U0 filtered to ^+ — so pre-existing markers in touched files stay quiet. (2) Tighten the marker set: match case-sensitively (drop -i), require the colon form TODO:/FIXME:/HACK:, and drop WORKAROUND and XXX from the list — in this codebase they are documentation vocabulary, not unfinished-work markers.

stop-verify.sh — fix bug

Fired twice in one session although it says "fires ONCE per session". The checklist itself was useful both times — the content needs no change.

The mechanism is a real bug, and it is not in this file: the once-per-session marker (*_stop-verify.done) lives in the shared stop-guards state directory, and the janitor in lib-stop-guard.sh line 110 runs on every Stop event:

find "$_MARKER_DIR" -type f -mmin +60 -delete 2>/dev/null

That deletes every state file older than an hour — including the done-marker of a session that is still running. Any session longer than an hour gets the full quality gate again (and again each hour). The same janitor also wipes the advisory-mode markers and streak counters of long sessions, so other stop hooks quietly get fresh budgets too.

Suggestion: age the cleanup by session length, not by hook activity — simplest is -mmin +1440 (a day), or exclude *.done files from the sweep. The one-hour value predates day-long sessions.

validate-style.ps1 (via pre-write-style.sh) — tune

Blocked three legitimate edits, all on the single-quote rule; it also enforced correctly several times (alignment, quoting), so the rules themselves earn their keep — two regexes just can't see context.

1. Single quotes inside double-quoted strings. The rule (?<![@])'.+' (line 46) fires on T-SQL literals nested in double-quoted PowerShell — "… like 'dbatools%' …", "… WITH MARK 'dbatoolstest' …" — which is exactly the style the repo wants. Stripping double-quoted spans before the check fixes all three observed blocks:

$stripped = $line -replace '"[^"]*"', '""'
if ($stripped -match $patternSingleQuote) { ... }

2. Inline hashtables never leave the state machine. @{ anywhere on a line enters hashtable tracking, but the exit pattern ^\s*\} requires a line starting with a brace. A one-line @{ Name = 1 } therefore opens the tracker and never closes it, and every later line containing = is collected until some unrelated closing brace ends the block — producing false "misaligned hashtable" reports far from any hashtable. Skipping tracking when the @{ line closes its own brace fixes it.

Known limitation, probably fine: an Edit's new_string includes unchanged context lines, so a pre-existing violation next to the edit blocks it. Distinguishing context from new code inside the hook is not really possible; re-anchoring the edit works around it and the cost has been small.

Suggestion: apply the two regex fixes; leave the rule set as is.

pre-edit-read-check.sh — leave

Blocked one edit after a context compaction, although the file had been Read earlier. Checked against source: this is designed behavior, not a reset bug — session-compact-reset-reads.sh clears the Read tracker on compact|resume precisely because the agent's memory of what it read is gone after compaction. One extra Read is the intended price. No change needed.

Remove-Item guard ("system path '*' is blocked") — not a repo hook

Blocked Remove-Item three times: twice on variable paths it could not resolve, once on a literal UNC path. Traced: this message exists nowhere in .claude/ — pre-bash-guard.sh only blocks rm on filesystem roots. The blocks come from Claude Code's own sandbox layer, so there is nothing here to tune; listed only so the inventory is complete.

What worked

  • pre-bash-commit-do.sh and stop-registration-check.sh fired only when correct all session.
  • The stop-guard budget did its job: no gate ever trapped the session, and the worst case was three rewrites.
  • The earlier infinite-loop problem was environmental (no JSON parser in Git Bash) and is fixed — jq is installed and hooks-doctor.sh reports clean.

Sample: one session, 2026-08-29, on Windows / Git Bash hooks; sources verified 2026-08-30.

This report was created by Claude and reviewed by Andreas Jordan.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions