v1.4.1
Fewer git/gh approval prompts in Claude Code, with the human kept at the protected-branch boundary.
Note
One visible behavior change: two more spellings of a tag push now ask under strict. Nothing to do on upgrade, but a session that ran git push --tags without a prompt will now get one. See Upgrading.
Highlights
A tag push asks under strict, however it's spelled. Three spellings of the same act disagreed: git push origin v1.3.0 asked, while git push origin refs/tags/v1.3.0 and git push --tags were auto-approved. ref_to_branch mapped any ref that isn't refs/heads/… to None, and push_decision reads None as "this side names no branch, nothing to object to" — so a fully-qualified tag ref sailed past every branch check into the strict auto-approve. Publishing a tag is a release, and a release is worth a keystroke. git push --follow-tags stays auto-approved on purpose: it publishes only annotated tags already reachable from the branch being pushed, and push.followTags can turn that on from config where the hook can't see it.
A non-interactive denial now reads as a denial. In auto, dontAsk, and bypassPermissions there is no human to answer a prompt, so confirm() flips a would-be ask into a deny — but it re-emitted the ask's wording verbatim, ending in "confirm before proceeding". An agent reads that as a prompt still waiting and retries a command it can never get approved from the session. Both paths now state the same cause and differ in the closing clause: the denial names the permission mode and says retrying won't help, so the agent hands the command off instead of looping on it.
Upgrading
No required steps. The version string is the only thing that moved; no configuration, hook registration, or decision default changes shape.
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-guardEither 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 will be visible. Under the default strict policy, git push origin refs/tags/vX.Y.Z and git push --tags now ask where they previously auto-approved. Under protected they defer, as before — that policy only guards main/master. If a release script drives Claude non-interactively, that ask becomes a deny by design, and the denial now says so in words rather than inviting a retry.
Everything since v1.4.0
Three PRs, all of them shipped below — nothing withheld from this list.
- fix: ask before a tag push under strict, however it's spelled by @karlkfi in #35
- fix: word a non-interactive denial as a denial, not a confirmation by @karlkfi in #34
- test: run the suite on Windows by @karlkfi in #36
Decision surface
One row is added to the behavior table and nothing is removed, so every command that was auto-approved at v1.4.0 still is, except the two tag-push spellings above:
| command | v1.4.0 | v1.4.1 |
|---|---|---|
git push origin v1.3.0 |
ask | ask |
git push origin refs/tags/v1.3.0 |
allow | ask |
git push --tags |
allow | ask |
git push --follow-tags |
allow | allow |
Reason wording is now part of the contract, not incidental. The suite asserts that the cause survives both paths, that an ask ends in "confirm before proceeding", and that a deny never does.
Validation
237 specs, green on Linux across Python 3.10–3.13 and — new this release — on Windows, on the release commit itself (run). The Windows job runs the same suite under Git Bash, which is the shell Claude Code's Bash tool uses there.
Windows was worth a job of its own because ntpath reads a leading slash as drive-relative: an MSYS-shaped path resolves onto the hook process's drive, git -C misses the repo, and a protected-branch ask degrades into no decision at all. That is the failure mode where the guard looks installed and enforces nothing.
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 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 tag-push fix tightens the guard rather than patching a reported vulnerability: a form that was silently auto-approved now asks. 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.0...v1.4.1