{"slug": "a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space", "title": "A bug in my own branch-guard hook, caught by typing one extra space", "summary": "A developer found a whitespace-parsing bug in their own Claude Code branch-guard hook after a week of use: the detection step accepted both `git push` and `git  push`, but the parser used `${command#*git push}`, a whitespace-sensitive parameter expansion that failed on doubled spaces, causing the hook to report the blocked branch as \"push\" instead of \"main\". The fix normalizes whitespace runs with `tr -s ' \\t'` and adds a regression test, taking the harness from 26 to 27 cases, shipped as v1.3.", "body_md": "**Context:** Last week I shipped a set of 4 Claude Code hooks that catch common agent mistakes — no-op edits, pushes to the wrong branch, out-of-scope writes, and unverified claims at the end of a turn. I wrote about them [in article #1](https://dev.to/98dkcc488bglitch/how-i-stopped-claude-code-from-lying-about-finishing-tasks-4-hooks-1-week-of-sanity-54pb) and the testing in [#2](https://dev.to/98dkcc488bglitch/26-test-cases-for-a-20-line-bash-function-and-what-each-one-taught-me-about-claude-code-gbc).\n\nThis piece is the small, embarrassing follow-up: a bug I found by dog-fooding the hooks on their own payloads.\n\n`block-wrong-branch.sh` runs on `PreToolUse(Bash)` for `git push`. It extracts the target branch and matches it against `.claude/branch-policy.json`. Simplified:\n\n```\n# Only inspect git push commands.\ncase \"$command\" in\n  *\"git push\"*|*\"git  push\"*) : ;;\n  *) exit 0 ;;\nesac\n\n# Tokens after \"git push\".\ntail=\"${command#*git push}\"\nread -r -a tokens <<< \"$tail\"\n# ...pick the branch out of the positional args...\n```\n\nThe detection step accepts both `git push` and `git  push` (I knew doubled whitespace shows up in real commands — people copy-paste). The parser step uses `${command#*git push}` to strip everything up to and including `git push`.\n\nSee the bug yet? I didn't, for a week.\n\nI was writing new test cases and typed one with a doubled space by accident:\n\n```\n{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git  push origin main\"}}\n```\n\nFed it to the hook:\n\n```\nexit 2\nstderr: Blocked: `git push` to branch \"push\" is not allowed by .claude/branch-policy.json.\n        Reason: not in allow-list [...].\n```\n\nThe block was correct — nothing matched the allow list, fail-safe wins. But the branch name it reported was `push`. Not `main`.\n\n`${command#*git push}` is pattern-matching for a literal `git push` with one space. The input had two. Shell parameter expansion doesn't collapse whitespace runs, so the prefix pattern never matched. `tail` was unchanged — still the full command `git  push origin main`.\n\nThen `read -r -a tokens <<< \"$tail\"` tokenizes it. Bash word-splitting *does* collapse whitespace, so tokens became `[git, push, origin, main]`. The parser picks the second positional argument as the branch → `push`.\n\nCase-statement glob = whitespace-sensitive. Parameter expansion = whitespace-sensitive. Word-splitting = whitespace-insensitive. Three different rules in twenty lines of bash. The detection step forgave the double space; the parser didn't.\n\nOne line, at the top of the command handling:\n\n```\n# Normalize runs of whitespace so `git  push` parses the same as `git push`.\ncommand=\"$(printf '%s' \"$command\" | tr -s ' \\t')\"\n```\n\nAdded a regression test (`\"git  push origin main\"` must report blocked branch `main`, not `push`). 26 → 27 tests. Shipped as v1.3.\n\nTwo things.\n\n**One:** the hook block was *correct* — the kit didn't let a bad push through. But the error message lied about *why*. That's exactly the behavior I built this tool to prevent in agents: not catastrophic wrong actions, just small, confident, wrong-shaped claims about what happened. \"Blocked branch push\" is a factually wrong feedback string the LLM would then try to reason about (\"hm, I didn't try to push a branch called `push`, did I?\"). Dog-fooding caught it before any user did.\n\n**Two:** when you build hook scripts for an LLM, the LLM is going to read your stderr and reason about it. Vague or wrong error strings don't just hurt the user — they poison the agent's next turn. \"Blocked: branch `push` not allowed\" is in some ways worse than no feedback, because now the agent is going to try to work around a constraint that doesn't exist.\n\nAdversarial inputs worth testing on your own hook parsers:\n\n`/`, `.`, or `-force` as a literal substring (not as a flag)`src:dst`\n`tool_input` key\nMy 27-test harness runs these in under 20ms. If your hooks don't have a harness, they'll have the bug.\n\n`PostToolUse(Edit|Write|MultiEdit)`, diffs the file against the index. Zero bytes changed → exit 2 with a correction prompt. Catches no-op edits where the LLM later claims \"I updated X.\"`PreToolUse(Bash)` for `git push`, parses the branch out of the command and matches it against `.claude/branch-policy.json`. Fail-safe: unknown branch, block.` PreToolUse(Edit|Write)`, matches the target path against allow/deny globs in `.claude/scope.json`. Fail-safe.` Stop`, scans the final assistant message for \"I updated/created/added X\" style claims and cross-references `trust-audit.log`. If the file isn't in the log, block with \"either make the change or correct your previous message.\" Has a `stop_hook_active` loop-guard — a reader of article #2 pointed out the missing one, that was v1.2.\nHooks + install script + 27-test harness + two policy JSON files. MIT. On [Gumroad](https://dumkestar6.gumroad.com/l/vicuep) with a launch coupon (`SHOWDEV50` → half off).\n\nIf you build hooks and your parser trips on something weird, reply with the payload — I'll run it against mine. That's the kind of feedback that made v1.1, v1.2, and v1.3 each better than the last.\n\n*This post was drafted with AI assistance and reviewed for accuracy.*", "url": "https://wpnews.pro/news/a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space", "canonical_source": "https://dev.to/98dkcc488bglitch/a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space-5b8l", "published_at": "2026-10-16 09:03:34+00:00", "updated_at": "2026-10-16 09:08:53.829025+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["Claude Code"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space", "markdown": "https://wpnews.pro/news/a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space.md", "text": "https://wpnews.pro/news/a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space.txt", "jsonld": "https://wpnews.pro/news/a-bug-in-my-own-branch-guard-hook-caught-by-typing-one-extra-space.jsonld"}}