Skip to main content

hook-prefix-matcher-skips-compound-command

A PreToolUse Bash hook that gates on a command PREFIX (`[[ "$COMMAND" != git\ commit* ]] && exit 0`) silently no-ops when you write a compound command like `git add -A && git commit …`. The action succeeds, the hook never ran, and you report "verification passed" when nothing was verified.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
blas1n/claude-skills
آخر نشاط في المصدر
٧ أغسطس ٢٠٢٦ في ١٨:٥٧
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٢
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
hook-prefix-matcher-skips-compound-command
description
A PreToolUse Bash hook that gates on a command PREFIX (`[[ "$COMMAND" != git\ commit* ]] && exit 0`) silently no-ops when you write a compound command like `git add -A && git commit …`. The action succeeds, the hook never ran, and you report "verification passed" when nothing was verified.
category
trap
# Hook prefix matcher skips compound commands ## Problem A common Claude Code guard runs the test suite before a commit: ```bash # ~/.claude/hooks/pre-commit-verify.sh COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""') if [[ "$COMMAND" != git\ commit* ]]; then exit 0 # not a commit — nothing to do fi ... uv run pytest "$TEST_DIR" || { echo "BLOCKED: Unit tests failed." >&2; exit 2; } ``` The matcher tests the **whole command string** against a prefix. So this fires: ```bash git commit -m "..." # matches → suite runs → can BLOCK ``` and this does not: ```bash git add -A && git commit -m "..." # does NOT match → exit 0 → suite NEVER runs ``` Same for `cd repo && git commit …`, `git add . ; git commit …`, and anything where `git commit` is not the leading token. The failure is silent and *inverted*: the guard exists to stop you, so its silence reads as approval. A commit that sails through feels verified. It isn't. Observed cost: a broken test rode through two commits and was caught only by CI, after the earlier attempt (`git commit` alone) had been correctly blocked — which made the later "pass" look like the fix had worked. ## Detection The hook's own output is the tell. A hook that ran prints its banner in the tool result: ``` [pre-commit] Running pytest... ``` **No banner ⇒ the hook did not run.** Before claiming a hook verified anything, look for its output. `git commit` succeeding is not evidence. Confirm the matcher directly when unsure: ```bash grep -n 'COMMAND' ~/.claude/hooks/<hook>.sh ``` ## Solution 1. **Own the verification; don't delegate it to a hook.** Run the gate yourself and read the result, then commit: ```bash uv run pytest tests/ -q # you see this pass git add -A git commit -F - <<'MSG' ... MSG ``` 2. **If you want the hook to fire, keep the matched token first.** Stage in a separate call so the commit command starts with `git commit`: ```bash git add -A # call 1 git commit -F - ... # call 2 — hook matches ``` 3. **Never phrase a report as "the hook passed"** unless you saw its banner. Say what you actually ran. 4. **If you own the hook**, make the matcher substring-based and anchor on the real intent: ```bash if ! grep -qE '(^|[;&|]\s*)git\s+commit\b' <<<"$COMMAND"; then exit 0; fi ``` ## Sibling failure: the hook runs but its tooling isn't installed The inverse also bites. In a **fresh git worktree** the venv has no dev dependencies, so the hook's `uv run pytest tests/` cannot spawn `pytest`, exits non-zero, and reports: ``` BLOCKED: Unit tests failed. Fix before committing. ``` Nothing failed — nothing ran. Reproduce the hook's exact command before believing its verdict: ```bash uv run pytest tests/ -q # → "error: Failed to spawn: `pytest`" uv sync --extra dev # then the hook's command works for real ``` Same lesson from the other direction: **a gate's verdict is only meaningful if you know the gate executed the thing it claims to have executed.** ## Generalisation Any guard keyed on surface form rather than effect has this hole — lint hooks matching `^npm run build`, deploy guards matching `^kubectl apply`, audit hooks matching `^psql`. The rule: **a silent guard is indistinguishable from a passing guard, so require positive evidence that it ran.** ## Related - `feedback_prompt_hooks_crash` — hook config shape (command type required) - `verification-before-completion` - `piped-gate-masks-exit-code` — the same "gate silently didn't gate" family
عرض على GitHub