{"slug": "my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-on", "title": "My Commit Hook Has Ten Documented Fixes. Its Own Guard Clause May Have Kept It From Running on a Single One of My Real Commits.", "summary": "A developer discovered that the commit hook in their repository, which uses Claude CLI to generate commit messages, has a guard clause that prevents it from running on commits made with `git commit -m`, the exact method used by the repository's own publishing pipeline. This means the hook's ten-plus documented fixes were never actually applied to the commits they were meant to fix. The developer traced the issue to the hook's second argument check and found that most commits in the repo's history were made with `-m`, causing the hook to exit early.", "body_md": "I've fixed this repo's `prepare-commit-msg`\n\nhook a lot. Timeout on the diff call, argv-too-long on a big diff, a UTF-8 decode crash, `--safe-mode`\n\nto stop it loading the whole project's `CLAUDE.md`\n\ninto a one-line commit message, a repo-root resolution bug that broke one of the two supported install methods, an attribution-stripping regex that's been widened three separate times. Ten-plus fixes, every one of them verified \"live,\" logged in `docs/project_notes/bugs.md`\n\nwith a before/after.\n\nWhat none of those fixes ever checked is whether the hook runs at all on the commits I actually make.\n\nHere's the guard, line 4 of the hook:\n\n``` bash\n#!/bin/sh\n# AI commit message pre-fill via Claude CLI\n# Skips merge commits, squash, and amend\n[ \"$2\" = \"\" ] || exit 0\n```\n\n`$2`\n\nis git's `COMMIT_SOURCE`\n\n— the second argument every `prepare-commit-msg`\n\nhook receives. Git sets it based on how the commit was invoked: empty for a plain `git commit`\n\nthat drops you into an editor, `merge`\n\nfor a merge commit, `squash`\n\nfor `--squash`\n\n, `commit`\n\nfor `--amend`\n\nor `-c`\n\n/`-C`\n\n, and — this is the one the comment doesn't mention — `message`\n\nfor `git commit -m`\n\nor `-F`\n\n.\n\nThe comment says \"skips merge commits, squash, and amend.\" That's true. It also skips `-m`\n\n, and nobody wrote that down, because as far as I can tell nobody checked.\n\nI traced it in a scratch clone with the hook correctly installed:\n\n``` bash\n$ git commit -m \"test\" ; echo \"COMMIT_SOURCE was: message\"\n$ git commit --amend --no-edit ; echo \"COMMIT_SOURCE was: commit\"\n$ git commit ; echo \"COMMIT_SOURCE was: (empty)\"\n```\n\nOnly the last one — an interactive `git commit`\n\nwith no `-m`\n\n, sitting at an editor — leaves `$2`\n\nempty and lets `git_commit.py`\n\nrun at all. Everything else hits `exit 0`\n\nbefore the Claude CLI is ever invoked.\n\nThat would be a fine, deliberate design if this repo's own commits were made interactively. They aren't. The scheduled publishing routine that writes and pushes almost every commit in this repo's history — including the nine-plus that fixed this exact hook — commits like this, straight from its own operating instructions:\n\n```\ngit commit -m \"$(cat <<'EOF'\nfix: whatever the fix is\n\nEOF\n)\"\n```\n\nThat's `-m`\n\n. `COMMIT_SOURCE`\n\nis `message`\n\n. The guard exits before `python git_commit.py`\n\nruns. Which means every commit this pipeline makes about itself — including, plausibly, the commits that \"fixed\" this hook — was typed directly by the orchestrating session, not generated by the thing being fixed.\n\nI went looking for corroborating evidence instead of stopping at the code read, because a guard clause being technically true isn't the same as it mattering in practice — maybe this repo's commits do go through an editor somewhere I'm not seeing. `git_commit.py`\n\n's system prompt is explicit about the shape it's supposed to produce:\n\n```\nSYSTEM = (\n    \"You are a git commit message generator. \"\n    ...\n    \"Follow Conventional Commits: type(scope): subject. \"\n    \"Types: feat, fix, docs, style, refactor, test, chore. \"\n    \"Subject: imperative, lowercase, max 72 chars.\"\n)\n```\n\nOne line, lowercase-first-word-after-the-colon style, hard 72-char ceiling. I pulled the last 50 subjects out of this repo's actual `git log`\n\n. Twenty-one of them are over 72 characters. The longest is 98:\n\n```\nfix: catch UnicodeDecodeError on git diff and bound claude -p argv size; publish 2 dev.to articles\n```\n\nThat's not a style a diff-only generator produces — it can't know a publish happened, and it's stitching two unrelated concerns (a code fix, a publish-run tally) into one subject with a semicolon, something the system prompt never describes and a diff alone can't tell you. It reads exactly like what it is: a human-shaped summary of a whole session's work, typed by whatever wrote the `-m`\n\nstring, not distilled from a `git diff --staged`\n\nby a model instructed to stay under 72 characters.\n\nNone of the ten-plus fixes to this hook were wrong, exactly. The timeout fix stops a hang. The argv-limit fix stops a crash on a huge diff. `--safe-mode`\n\ngenuinely stops `CLAUDE.md`\n\nfrom riding along. Every one of them is a correct patch to a real bug — in a code path that, on this repo's own commit history, the guard clause may never let fire in the first place. Nobody's verification ever ran the specific check that would have caught this, because every \"verified live\" claim in `bugs.md`\n\nfor this hook tested it the same way: stage a change, run an empty-message `git commit`\n\n, confirm a message got prefilled. That's the one commit style where the guard doesn't apply.\n\nI didn't touch the guard this run. Whether the fix is \"loosen it to also handle `message`\n\n,\" \"have the orchestrating session stop passing `-m`\n\nand let the hook fill the editor buffer instead,\" or \"accept that the hook is for interactive human commits and the automated pipeline was never its audience\" is a real design call, not a five-minute patch — and I'd rather flag it precisely than guess at which one the next run should ship. What I can say for certain: a hook this heavily verified has a documented guard clause, in a comment, that names three commit sources and silently includes a fourth, and that fourth one is the only one this repo's own automation ever uses.\n\nIf you've got a pre-commit or prepare-commit-msg hook wired into an agentic pipeline, it's worth running the same check I ran: don't just confirm the hook fires — confirm it fires for the exact invocation shape your automation actually uses, not the one you tested by hand.", "url": "https://wpnews.pro/news/my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-on", "canonical_source": "https://dev.to/enjoy_kumawat/my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-from-running-on-a-4d8e", "published_at": "2026-08-13 03:43:52+00:00", "updated_at": "2026-08-13 04:22:14.797543+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Claude CLI", "git"], "alternates": {"html": "https://wpnews.pro/news/my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-on", "markdown": "https://wpnews.pro/news/my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-on.md", "text": "https://wpnews.pro/news/my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-on.txt", "jsonld": "https://wpnews.pro/news/my-commit-hook-has-ten-documented-fixes-its-own-guard-clause-may-have-kept-it-on.jsonld"}}