Skip to content

fix(guardrails): harden the commit-msg hook and add a config-hook install lane #5948

Open
2 of 5 issues completed
Open

Description

@kyle-sexton

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:

  1. Windows IDE git. The shipped hook and its resolver call bash by name. Git bundled with
    Visual Studio and SSMS ships no bash.exe, so (inferred) the hook would resolve to the WSL
    launcher and fail.
  2. Merge and revert subjects. git merge runs commit-msg, and the shipped hook exempts only
    fixup!/squash!/amend!. Under the bundled Conventional Commits pattern, Merge branch ...
    and Revert "..." do not match, so a non-fast-forward merge or a revert would be rejected.
  3. Install lane. The installer copies into each repo's .git/hooks and refuses repos managed by
    lefthook, husky, pre-commit or core.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
    includeIf user config, covering every clone and linked worktree under it.
  4. 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.sh line 1 #!/usr/bin/env bash, line 38
    uses BASH_SOURCE, lines 45 and 55 call bash explicitly, lines 78-79 exempt only
    fixup! /squash! /amend! .
  • plugins/guardrails/hooks/resolve-convention-pattern.sh line 50: the bundled pattern is
    ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+, which
    Merge branch 'x' and Revert "feat: y" do not match.
  • Same file, lines 15-26: enforcement reads the team-tracked .claude/source-control.md only; 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 committed core.hooksPath lane is deliberately not scaffolded.
  • Git's own githooks documentation (git 2.55): commit-msg "is invoked by git-commit and
    git-merge". git-hook documentation describes hook.<friendly-name>.event and .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/sh with a POSIX body, or a sh entry that execs a bash it
    has checked is not a System32 path. Never call bash by 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:setup option that writes a user-scope [hook "<name>"] block (events
    commit-msg) under an includeIf the 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
    is docs/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 ERE subject_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 bash in 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' and Revert "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.exe is bash in POSIX mode, so bashisms pass on Windows and break only on Linux dash.
    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 from scripts/sync-resolve-convention-pattern.sh and CI rejects drifted copies (see
    the resolver header). Edit the root file and re-sync. block-noncanonical-commit and
    block-convention-violation use the same resolver, so a POSIX rewrite changes their runtime too;
    run their suites in step A.
  • Do not suggest --no-verify in failure text; block-no-verify refuses 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

  1. added
    status: needs-decisionAwaiting a human or maintainer judgment call.
    priority: mediumReal value, no hard deadline; normal backlog flow.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    on Oct 2, 2026
  2. kyle-sexton commented on Oct 13, 2026

    @kyle-sexton
    MemberAuthor

    Decision recorded. The owner approved applying independently verified recommendations (high-risk set, reviewed) on 2026-10-13.

    • Ship step A (sh entry point, exempt Merge and Revert subjects, tests)? → yes, now
      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)
    • Add a config-hook install lane (hook..event/command under an includeIf)? → yes, documenting the git 2.54 floor
      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
    • May a user-private conventions layer enforce where the team tracks none? → yes, additive only, never weakening a team pattern
      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:

    • Q3 changes a documented contract: resolve-convention-pattern.sh says a blocking gate never reads the user/local overlays and an un-opted repo gets no enforcement. The issue itself proposes amending docs/conventions/commit-convention, so the recommendation is legitimate, but the PR must amend both that convention and the resolver header.

    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.

  3. kyle-sexton commented on Oct 13, 2026

    @kyle-sexton
    MemberAuthor

    Owner decision (2026-10-13): approved the recommendation.

    • Is step A still needed on main? → Yes, and it ships first as its own PR: sh entry point, exempt git-generated Merge and Revert subjects, tests. On main, commit-msg-convention.sh still starts with #!/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)
    • Step B: config-hook install lane? → Yes, with the git 2.54 floor. Tests gate on the git version, and the docs say what happens below 2.54.
      Basis: https://git-scm.com/docs/git-hook/2.54.0 vs /2.53.0; git RelNotes 2.54.0
    • How does the user conventions layer land? → Fallback-only: a user pattern applies only when the team tracks none and is never merged with a team pattern. Edit the canonical lib/resolve-convention-pattern.sh and regenerate the plugin copy with scripts/sync-shared-copies.sh. Amend the resolver's policy-floor header (lines 17-28) and docs/conventions/commit-convention together, and test all three consumers: block-noncanonical-commit, the Claude Code content gate and the commit-msg hook.
      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.

  4. kyle-sexton commented on Oct 13, 2026

    @kyle-sexton
    MemberAuthor

    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.

  5. added
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Oct 13, 2026
  6. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Oct 14, 2026
  7. kyle-sexton commented on Oct 18, 2026

    @kyle-sexton
    MemberAuthor

    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 bash by 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: fix(guardrails): let git-generated Merge and Revert subjects pass the commit-msg hook (fix(guardrails): let git-generated Merge and Revert subjects pass the commit-msg hook #7966)
    • not filed: fix(git-hooks): run hook entries and the commit-msg hook without calling bash by name: allowlist: sync-shared-copies
    • not filed: feat(git-hooks): install hook entries as git config-based hooks under an includeIf, from git 2.54: allowlist: sync-shared-copies
    • not filed: feat(guardrails): enforce a user-private commit convention only where the team tracks none: allowlist: sync-shared-copies

    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.

  8. kyle-sexton commented on Oct 24, 2026

    @kyle-sexton
    MemberAuthor

    #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.yaml and 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: ready and admission-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).

  9. removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 24, 2026
  10. github-actions commented on Oct 25, 2026

    @github-actions
    Contributor

    The triage sweep dispatched intake triage for this issue (attempt 1 of 2).

  11. github-actions commented on Oct 25, 2026

    @github-actions
    Contributor

    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 bash by 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.

  12. changed the issue type fromtoon Oct 25, 2026
  13. melodic-automation-lanes commented on Oct 25, 2026

    @melodic-automation-lanes
    Contributor

    Not admitted to the issue lane yet: Size large: the split into sub-issues at intake is pending (owner Q93).

  14. changed the issue type fromtoon Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: mediumReal value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Fields

    No fields configured for Feature.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions