Repository navigation
fix(guardrails): harden the commit-msg hook and add a config-hook install lane #5948
Description
Part of #5943
Problem
guardrails already ships a commit-msg convention hook (lib/git-hooks/commit-msg-convention.sh,
installed by /guardrails:setup apply install-commit-msg). A working machine-local gate covers four
things it does not:
- Windows IDE git. The shipped hook and its resolver call
bashby name. Git bundled with
Visual Studio and SSMS ships nobash.exe, so (inferred) the hook would resolve to the WSL
launcher and fail. - Merge and revert subjects.
git mergerunscommit-msg, and the shipped hook exempts only
fixup!/squash!/amend!. Under the bundled Conventional Commits pattern,Merge branch ...
andRevert "..."do not match, so a non-fast-forward merge or a revert would be rejected. - Install lane. The installer copies into each repo's
.git/hooksand refuses repos managed by
lefthook, husky, pre-commit orcore.hooksPath. Git's config-based hooks (hook.<name>.event/
hook.<name>.command) run beside those managers and can be wired once per tree through an
includeIfuser config, covering every clone and linked worktree under it. - User-private conventions. Enforcement reads only the team-tracked layer. An operator cannot
gate their own commits in a repo whose team tracks no convention without changing the repo.
Evidence
Verified on origin/main:
plugins/guardrails/lib/git-hooks/commit-msg-convention.shline 1#!/usr/bin/env bash, line 38
usesBASH_SOURCE, lines 45 and 55 callbashexplicitly, lines 78-79 exempt only
fixup!/squash!/amend!.plugins/guardrails/hooks/resolve-convention-pattern.shline 50: the bundled pattern is
^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+, which
Merge branch 'x'andRevert "feat: y"do not match.- Same file, lines 15-26: enforcement reads the team-tracked
.claude/source-control.mdonly; the
user-global and local overlays are drafting inputs by design ("policy floor"). plugins/guardrails/lib/git-hooks/commit-msg-convention.test.sh: no merge or revert case.plugins/guardrails/skills/setup/context/install-commit-msg.md: managed-repo preflight refuses;
a committedcore.hooksPathlane is deliberately not scaffolded.- Git's own
githooksdocumentation (git 2.55):commit-msg"is invoked by git-commit and
git-merge".git-hookdocumentation describeshook.<friendly-name>.eventand.command.
Not reproduced (inferred): the MinGit failure. The source item's analysis, from git's
compat/mingw.c: git keeps only the shebang's basename and searches PATH, the bundled MinGit has
sh.exe but no bash.exe, so bash lands on the System32 WSL launcher, which runs a Linux
/bin/bash that cannot see the Windows hook path. sh exists in every git distribution and has no
System32 counterpart. The item also states the config-hook floor is git 2.54 and that older gits
ignore hook.* silently; the 2.54 number was not checked this pass.
Proposed approach
Ship in two steps; step A can land first.
A. Hardening (agent-ready once accepted):
- Hook and resolver entry point
#!/bin/shwith a POSIX body, or ashentry thatexecs a bash it
has checked is not a System32 path. Never callbashby name. - Exempt git-generated subjects:
Merge,Revert ", plus the existing autosquash forms. Add
merge and revert test cases.
B. Config-hook lane (needs decision):
- A
/guardrails:setupoption that writes a user-scope[hook "<name>"]block (events
commit-msg) under anincludeIfthe operator chooses, pointing at a stable copy of the hook,
instead of copying into.git/hooks. Works in managed repos because it does not touch them. - Decide whether a user-private conventions layer may enforce. It can only add enforcement where
the team tracks none, never weaken a team pattern, which keeps the policy floor. The owner contract
isdocs/conventions/commit-convention; amend it there if accepted. Shape from the local gate: an
allowlist where a repo is gated only when a per-repo file (keyed by the canonical clone name from
git rev-parse --git-common-dir) holds one POSIX EREsubject_pattern. - Bypass that never locks out: one-off env var and
git -c hook.<name>.enabled=false, documented.
Overlap: the agent-side block-convention-violation guard and this hook should keep reading one
resolver so the two layers cannot drift.
Acceptance criteria
- No hook or resolver file names
bashin its shebang or as a command; a lint or test asserts it. - Hook body passes under
dash(POSIX), tested in CI on Linux. -
Merge branch 'x'andRevert "feat: y"pass under the Conventional Commits pattern; a
normal violating subject still fails. - (B) Config-hook install is idempotent, shows in
git hook list commit-msg, and runs beside a
lefthook-managed repo's own hooks in a test repo under the scratchpad. - (B) The conventions-layer decision is recorded in
docs/conventions/commit-convention.
Constraints and gotchas
- MSYS
sh.exeis bash in POSIX mode, so bashisms pass on Windows and break only on Linuxdash.
Test on Linux. - Gits older than the config-hook floor ignore
hook.*silently (the item names SSMS MinGit,
some IDE-bundled gits and a distro apt git). Document it; do not claim coverage there. - The resolver's single source of truth is the root
lib/resolve-convention-pattern.sh; plugin
copies come fromscripts/sync-resolve-convention-pattern.shand CI rejects drifted copies (see
the resolver header). Edit the root file and re-sync.block-noncanonical-commitand
block-convention-violationuse the same resolver, so a POSIX rewrite changes their runtime too;
run their suites in step A. - Do not suggest
--no-verifyin failure text;block-no-verifyrefuses it in Claude sessions.
Context
Source: local handoff item 20260928-generalize-local-guards-into-plugins.md, candidate 4. Related:
block-convention-violation, docs/conventions/commit-convention.
Activity
kyle-sexton commentedon Oct 13, 2026
Decision recorded. The owner approved applying independently verified recommendations (high-risk set, reviewed) on 2026-10-13.
Basis: plugins/guardrails/lib/git-hooks/commit-msg-convention.sh:1,45,55,79; https://git-scm.com/docs/githooks (commit-msg 'invoked by git-commit and git-merge', fetched 2026-10-12)
Basis: https://git-scm.com/docs/git-hook/2.54.0 vs /2.53.0 (friendly-name config present only from 2.54, fetched 2026-10-12); plugins/guardrails/skills/setup/context/install-commit-msg.md
Basis: judgment; plugins/guardrails/hooks/resolve-convention-pattern.sh:15-26 (policy floor)
The shipped commit-msg hook calls bash by name and rejects git-generated Merge/Revert subjects; the issue also asks for a git config-hook install lane and a user-private conventions layer. All gaps verified unchanged on main.
Verification notes:
Verified by an independent fresh-context check (confirmed, risk high): gh issue view 5948 (OPEN, 0 comments; body item 4 and step B); origin/main plugins/guardrails/lib/git-hooks/commit-msg-convention.sh:1,40-60,72-85 (bash shebang, bash $RESOLVER, only fixup!/squash!/amend! exempt); origin/main plugins/guardrails/hooks/resolve-convention-pattern.sh:10-28; https://git-scm.com/docs/githooks commit-msg: 'invoked by git-commit[1] and git-merge[1]' (fetched 2026-10-12); https://git-scm.com/docs/git-hook/2.54.0 (14 friendly-name matches, hook..event/command) vs /2.53.0 (0); origin/main plugins/guardrails/skills/setup/context/install-commit-msg.md exists
Admission to an agent lane stays with the work-class rules and owner ratification.
kyle-sexton commentedon Oct 13, 2026
Owner decision (2026-10-13): approved the recommendation.
#!/usr/bin/env bash, calls bash by name at lines 45 and 55, and exempts only fixup!, squash! and amend! at line 79. git merge runs commit-msg, so a Merge subject is rejected today.Basis: origin/main plugins/guardrails/lib/git-hooks/commit-msg-convention.sh:1,45,55,79; https://git-scm.com/docs/githooks (commit-msg is invoked by git-commit and git-merge)
Basis: https://git-scm.com/docs/git-hook/2.54.0 vs /2.53.0; git RelNotes 2.54.0
Basis: origin/main plugins/guardrails/hooks/resolve-convention-pattern.sh:1-28 (GENERATED copy; policy floor); decision comment 2026-10-13T02:33:08Z
Next: an agent lane, via admission, ships step A as one PR, then step B with the user layer as a second PR after A merges. Both change a commit gate, so a human reviews and merges them.
This was generated by AI.
kyle-sexton commentedon Oct 13, 2026
Owner ratified this admission in an attended session on 2026-10-13: work-class: scoped. It touches a guard, CI script, settings or trust surface, so an agent may build it but the owner merges the PR: put 'Hold: a human merges this' in the PR body.
kyle-sexton commentedon Oct 18, 2026
This was generated by AI during backlog shaping.
This issue is split into 4 pieces, each sized for one lane PR. Every gap still holds on origin/main, but #7627 moved the hook to plugins/guardrails/git-hooks/ and added a shared installer whose dispatcher runs every entry with
bashby name. That puts the sh-entry and config-hook work in a shared-copies canonical file (lib/git-hooks/install.mjs, 10 copies), and the user layer in another (lib/resolve-convention-pattern.sh). Only the Merge/Revert exemption fits the lane. Owner decisions are recorded (2026-10-13 Decision recorded comment), and the owner merges every child.Filed pieces are sub-issues of this one, and the paced queue admits them to the lane. Unfiled pieces wait on the blocker shown. The owner approved this split in an attended backlog-shaping session on 2026-10-18.
kyle-sexton commentedon Oct 24, 2026
#7966 (Merge and Revert subjects) is closed. The 3 remaining pieces are filed, plus a Windows-host slice. C12 removed the sync-shared-copies allowlist hold. The user-private layer waits on #5906's resolver slice, so it reads
docs/conventions/source-control.yamland the ADR 0075 personal layer. Order: the sh entry first, then the config-hook lane and the user layer, then the Windows and parity check.Each agent slice is labelled
status: readyandadmission-deferred, with Size, Complexity and Uncertainty set; admission takes unblocked slices oldest first.Posted by AI while splitting the backlog (owner decisions Q93 to Q95 and C12, 2026-10-24).
github-actions commentedon Oct 25, 2026
The triage sweep dispatched intake triage for this issue (attempt 1 of 2).
github-actions commentedon Oct 25, 2026
Proposed work class: work-class: scoped
Basis: automated intake triage; the lanes App applied this class.
Size: Large
Complexity: Medium
Uncertainty: Knowable
Flags: none
Blocked by: none
Understood: harden the guardrails commit-msg hook so it never calls
bashby name, and add a config-based hook install lane plus an opt-in user-private convention layer. The maintainer's split comment says the merge/revert subject piece (#7966) is already closed. The remaining work is tracked as #9774, #9775, #9776 and #9778. This issue is too large for one change, so it looks like an umbrella for those slices. Nothing is missing from the report. I found no likely duplicates beyond those linked slices.Automated triage, applied by the lanes App.
melodic-automation-lanes commentedon Oct 25, 2026
Not admitted to the issue lane yet: Size large: the split into sub-issues at intake is pending (owner Q93).