Skip to content

feat(guardrails): opt-in git commit-msg hook for tool-agnostic subject enforcement - #1077

Merged
kyle-sexton merged 1 commit into
mainfrom
feat/919-optin-commit-msg-hook
Jul 22, 2026
Merged

feat(guardrails): opt-in git commit-msg hook for tool-agnostic subject enforcement#1077
kyle-sexton merged 1 commit into
mainfrom
feat/919-optin-commit-msg-hook

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

Audit f1's depth layer / f4's backstop (#912): a git commit-msg hook enforcing the team-tracked subject pattern regardless of tool or shell — editor commits, git commit -F <file>, IDE integrations, humans outside Claude — where the Claude-Code-layer gates see nothing.

New installable template lib/git-hooks/commit-msg-convention.sh + /guardrails:setup apply install-commit-msg (explicit opt-in; bare apply still writes nothing):

  • Chain-or-refuse. Managed repos (core.hooksPath, lefthook, husky, pre-commit) → refused with the manager-side remediation. An existing commit-msg hook is never overwritten: chained as commit-msg.pre-guardrails (runs first, its rejection is final) or the install refuses. Covers the operator's machine-local commit-msg gate.
  • Sentinel-marked (guardrails-commit-msg-convention) so convention-inference tooling excludes the installed hook as a signal — it is derived FROM the tracked config; counting it would echo-cycle. Sentinel re-install is idempotent.
  • Personal .git/hooks/ lane only. core.hooksPath, hook-manager configs, and tracked files are never touched — the committed team lane is a human PR decision, and core.hooksPath changes are exactly the shape block-no-verify refuses (interaction resolved by not going there).
  • Unresolved = no enforcement (never the bundled CC default); resolver copy removed → fail open, never block blind; fixup!/squash!/amend! exempt (autosquash).
  • Deadlock designed out. The rejection message instructs fixing the subject and never suggests --no-verify (which block-no-verify refuses in-session anyway); in Claude sessions the CC-layer gate (feat(guardrails): CC-layer content gate — commit subject + PR title #914/PR feat(guardrails): CC-layer content gate — commit subject + PR title #1071) blocks first, making this hook the cross-tool backstop.

Reads the same resolver contract as the CC-layer gate (copied beside the hook at install time — an installed consumer-repo hook cannot participate in the sync seam, so it carries its own unedited copy).

guardrails 0.11.00.12.0 with CHANGELOG entry.

Test plan

  • New 15-case contract suite lib/git-hooks/commit-msg-convention.test.sh — unresolved/PCRE pass-through, enforce block/allow, comment-line skipping, autosquash exemptions, empty message, resolver-removed fail-open, chain rejection-final + pass-through, sentinel presence
  • scripts/check-changed-skills.sh main — setup skill PASS (0 errors)
  • scripts/check-changelog-parity.sh --check-bump main — pass
  • shellcheck clean; exec bits set

Related

🤖 Generated with Claude Code

…t enforcement

Closes #919 (audit umbrella #912, f1 depth layer / f4 backstop).

New installable git hook template lib/git-hooks/commit-msg-convention.sh
+ /guardrails:setup apply install-commit-msg action (explicit opt-in;
bare apply still writes nothing). The hook validates every commit's
subject in the repo against the team-tracked pattern via a copy of the
shared enforcement resolver — editor commits, -F <file>, IDEs, humans
outside Claude.

Trust-surface contract per the issue: chain-or-refuse (managed repos
refused with manager-side remediation; existing commit-msg hooks chained
as commit-msg.pre-guardrails with final rejection authority, never
overwritten — including the operator's machine-local gate);
sentinel-marked for convention-inference exclusion (echo-cycle guard);
personal .git/hooks lane only — core.hooksPath and tracked files never
touched (team lane is a human PR decision, and hooksPath changes are the
shape block-no-verify refuses); unresolved = no enforcement, resolver
removed = fail open; fixup!/squash!/amend! exempt; rejection message
instructs fixing the subject and never suggests --no-verify (the
designed exit from the block-no-verify deadlock).

15-case contract suite; guardrails 0.11.0 -> 0.12.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton enabled auto-merge (squash) July 22, 2026 21:18
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Warning

Automated review did not complete — this is an infrastructure failure, not a review verdict.

Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."

Re-running the job, or pushing a new commit, will retry the review.

@github-actions

Copy link
Copy Markdown

Warning

Automated security review did not complete — this is an infrastructure failure, not a review verdict.

Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."

Re-running the job, or pushing a new commit, will retry the review.

@kyle-sexton
kyle-sexton merged commit a86c3aa into main Jul 22, 2026
25 checks passed
@kyle-sexton
kyle-sexton deleted the feat/919-optin-commit-msg-hook branch July 22, 2026 21:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6ecfb7eac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +95 to +98
**Install (on a clean preflight):** copy `${CLAUDE_PLUGIN_ROOT}/lib/git-hooks/commit-msg-convention.sh`
to `<git-dir>/hooks/commit-msg` and `${CLAUDE_PLUGIN_ROOT}/hooks/resolve-convention-pattern.sh`
to `<git-dir>/hooks/guardrails-resolve-convention.sh` (resolve `<git-dir>` via
`git rev-parse --absolute-git-dir` — in a worktree `.git` is a file), `chmod +x` both.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install hooks at Git’s resolved hook path

When this setup is run from a linked worktree, git rev-parse --absolute-git-dir resolves to the per-worktree admin dir (for example .git/worktrees/<name>), but Git resolves hooks through git rev-parse --git-path hooks/commit-msg, which points at the common hooks directory. I verified a commit in a linked worktree ignores a hook copied under the absolute git dir, so apply install-commit-msg can report success while normal commits in that worktree remain unenforced; use Git’s --git-path hooks/... result for the install/chain targets instead.

Useful? React with 👍 / 👎.

Comment on lines +65 to +66
[[ -n "$line" ]] || continue
[[ "${line:0:1}" == "$comment_char" ]] && continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate comment-prefixed commit subjects

For commits whose real subject begins with the configured comment character, this skips that subject as if it were template commentary; git commit -m '# bad' (and git commit -F file) preserve that line as the commit subject, with cleanup controlled separately (git commit -h lists -m/-F message inputs and a separate --cleanup option). With a ticket-pattern config I verified git commit -m '# definitely invalid' is accepted and recorded, and a -F file whose first line is invalid #... but later line matches is also accepted, so the new backstop can be bypassed by a comment-prefixed subject.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(guardrails): opt-in git commit-msg hook for tool-agnostic subject enforcement

1 participant