Skip to content

v1.4.2

Latest

Choose a tag to compare

@karlkfi karlkfi released this 03 Aug 22:12

Fewer git/gh approval prompts in Claude Code, with the human kept at the protected-branch boundary.

Warning

On Windows the guard was starting nothing at all. Up to and including v1.4.1 the hook was launched with a bare python3, which on Windows usually resolves to the Microsoft Store alias stub — it exits without running Python, and Claude Code treats a failed PreToolUse hook as non-blocking, so the tool call fell through to the session's own permission rules with no guard decision at all. If you run Claude Code on Windows, update. macOS and Linux were unaffected.

Highlights

The hook now finds a Python that actually runs. hooks/hooks.json invoked python3 "${CLAUDE_PLUGIN_ROOT}/hooks/branch-guard.py" directly — fine wherever python3 is a real interpreter, wrong on Windows, which ships a python3.exe alias stub that is always on PATH and fails at run time unless Python came from the Microsoft Store (exit 9009 under cmd.exe). Checking for presence does not catch it: where python3 and command -v python3 both find the stub. The new hooks/run-python-hook.cmd is a bash/cmd polyglot that resolves the interpreter by executing candidates and reading the exit code — py -3, python, python3 on Windows, python3 then python elsewhere, with python3 probed last precisely because it is the name most likely to be the stub. When none of them work it writes this guard is NOT enforcing to stderr rather than degrading quietly. Thanks to @smoochy for the fix — their first contribution to the project.

A launcher regression can no longer pass CI. The launcher is now the only route the suite takes to the hook: all 240 specs invoke hooks/run-python-hook.cmd branch-guard.py, the exact command hooks/hooks.json registers, instead of a bare interpreter. Previously a stray CRLF, a batch-label typo, or a dropped exec bit would have stayed green on every job while every real hook invocation failed — the same shape as the Windows bug, invisible for the same reason. A startup check aborts the run when the launcher's recorded mode is wrong, because a launcher that dies emits nothing and the harness reads nothing as a legitimate defer.

Upgrading

No required steps, and no decision changed. hooks/branch-guard.py is byte-identical to v1.4.1 — the classifier, the push policies, and PROTECTED_BRANCH_RE did not move, so every command auto-approved at v1.4.1 still is. What changed is how the hook gets started.

Third-party marketplaces have auto-update off by default, so an install pins its version until you turn it on or update by hand (Upgrade):

claude plugin marketplace update branch-guard
claude plugin update branch-guard@branch-guard

Either way a restart is needed — the hook is registered at startup, so a running session stays on the version it loaded.

One change that needs no action, but may be visible. The requirement is now "a working Python 3 on PATH" rather than specifically python3, so a machine that only has py -3 or python is now supported. If none of them runs, the launcher exits 1 and writes two lines to stderr naming the problem — on a Windows machine where the guard was never running, that message is the first sign of it.

Everything since v1.4.1

Five PRs, all of them shipped below — nothing withheld from this list. Four of the five are test and docs work that ships in no artifact; they are listed because the list is short enough to be complete.

  • fix: resolve Python interpreter portably so the hook runs on Windows by @smoochy in #32
  • test: drive the suite through the hook launcher by @karlkfi in #38
  • test: cover the launcher's missing-script error path by @karlkfi in #39
  • docs: codify the release-notes section menu by @karlkfi in #37
  • docs: require measuring a coverage claim before documenting it by @karlkfi in #40

Validation

240 specs, green on Linux across Python 3.10–3.13 and on Windows, on the release commit itself (run).

Every spec reaches the hook through the launcher, so it is covered by all 240 cases rather than by nothing. The two jobs are what cover both halves of the polyglot: Git Bash hands a .cmd to the Windows command processor, so windows-latest runs the batch branch while the Linux matrix runs the POSIX tail. That split is asserted rather than assumed — a fixture feeds the launcher a missing script name and checks both that it fails loudly and which path form comes back, since %~dp0 yields backslashes and the POSIX pwd yields forward slashes. If the routing ever flipped, the cmd.exe half would lose its only coverage without a single decision fixture going red.

What the suite does not assert: it drives the hook's JSON contract in a throwaway repo, not Claude Code's own permission plumbing, and it never exercises the no-interpreter path on a machine that genuinely lacks Python 3, because CI always has one. MSYS-shaped paths typed by hand in Git Bash still make the hook defer rather than decide — it over-defers, never silently allows.

Security

No advisory, and no dependencies to bump — the hook is stdlib-only Python and the plugin ships no third-party code.

The Windows fix is security-relevant even without an advisory, because a guard that fails open is worse than no guard: the operator believes it is there. On an affected Windows machine the protected-branch and destructive-command asks never fired, and whether anything else stopped the command came down to the session's own permission rules — a session with broad Bash allow-rules is exactly the setup branch-guard exists to backstop. None of the decisions changed in this release; they now get made.

Guarantees stay best-effort by design. Claude Code ignores hook decisions entirely under bypassPermissions, so anything you need to hold unconditionally still belongs in a git pre-push hook or server-side branch protection.

Full changelog: v1.4.1...v1.4.2