Bug
Commit message validation is silently bypassed for all commits. The commit-msg hook at .husky/commit-msg is never executed.
Root Cause
core.hookspath in .git/config points to .husky/_, but that directory does not exist:
core.hookspath = .husky/_ ← directory missing → Git silently skips all hooks
.husky/commit-msg ← actual hook, never runs
.husky/_ is generated by Husky's prepare script at npm ci time and is gitignored. When node_modules is cleaned up, .husky/_ is removed but .git/config retains the stale path. All subsequent commits — including from pipeline agent worktrees — have no enforcement.
There is also an underlying mismatch: Husky v9 (which is installed) sets core.hooksPath to .husky, not .husky/_. The _ subdirectory convention is a Husky v8 artifact. The stale config entry may have originated from a Husky v8 migration.
Impact
- Commit message format rules (issue number prefix, lowercase, under 80 chars) go unenforced
- Known affected commits:
da12ea7, 49958bf (subject line is only the Co-Authored-By trailer)
Fix
- Run
npm run prepare to regenerate .husky/_ and update core.hooksPath to the correct Husky v9 value (.husky)
- Verify with
git config core.hookspath — should output .husky
- Test by attempting a commit with a bad message and confirming it is blocked
- Document in
.claude/docs/ENGINEERING.md or CLAUDE.md that npm ci must be run after cloning / cleaning the repo to activate hooks
Implementation Plan
Overview
Hooks are dead in this checkout because node_modules/husky is not installed, so the prepare script never regenerated .husky/_ — Git's core.hooksPath=.husky/_ then points at a missing directory and Git silently skips all hooks. The fix is to make hook activation reliable and self-documenting, not just re-run prepare once: the real failure mode is that hooks depend on per-clone .git/config + a regenerated, untracked .husky/_, which fresh clones and agent worktrees don't reliably have. We reactivate hooks, add a fast self-check that surfaces a broken/missing hook setup, and document the activation requirement where worktree/clone setup is already covered.
Correction to the ticket's diagnosis (do not implement the ticket's step 1–2 verbatim): Husky v9 intentionally sets core.hooksPath to .husky/_ (the _ directory is v9's own bootstrap dir holding husky.sh and per-hook wrapper scripts that source the real .husky/<hook>). So .husky/_ is the correct v9 value — it is not a v8 artifact, and prepare will not change it to .husky. The bug is solely that .husky/_ was never (re)generated in this checkout because husky isn't installed. Verification must therefore expect .husky/_, not .husky.
Changes
package.json — add a hooks:check script that verifies core.hooksPath resolves to an existing directory containing the bootstrap, so a dead hook setup is detectable (and runnable in pre-push). No dependency changes.
CLAUDE.md — under the existing Worktrees & local dev section, state that npm ci (which runs prepare) is what activates Git hooks, that .husky/_ is regenerated and untracked, and how to re-activate if hooks stop firing.
- (Local-only, not a committed change) regenerate
.husky/_ by running npm ci / npm run prepare so hooks fire again on this machine and in each worktree.
No source/schema/server-action/UI changes — this is a tooling/config + docs ticket. ENGINEERING §1–8 (actions, data, UX states, a11y) do not apply.
Implementation
Testing
Risks / notes
- The ticket's fix steps 1–2 are factually wrong about the target value (
.husky vs .husky/_) — follow this plan's corrected verification. Setting the path to .husky manually would break the v9 bootstrap.
core.hooksPath lives in per-clone .git/config and is not shared across clones/worktrees — there is no purely-committed way to force it; activation will always depend on npm ci/prepare running per checkout. hooks:check + the doc note are the mitigation, not a hard guarantee.
- The two known bad commits (
da12ea7, 49958bf) are historical and are not rewritten here.
Bug
Commit message validation is silently bypassed for all commits. The
commit-msghook at.husky/commit-msgis never executed.Root Cause
core.hookspathin.git/configpoints to.husky/_, but that directory does not exist:.husky/_is generated by Husky'spreparescript atnpm citime and is gitignored. Whennode_modulesis cleaned up,.husky/_is removed but.git/configretains the stale path. All subsequent commits — including from pipeline agent worktrees — have no enforcement.There is also an underlying mismatch: Husky v9 (which is installed) sets
core.hooksPathto.husky, not.husky/_. The_subdirectory convention is a Husky v8 artifact. The stale config entry may have originated from a Husky v8 migration.Impact
da12ea7,49958bf(subject line is only theCo-Authored-Bytrailer)Fix
npm run prepareto regenerate.husky/_and updatecore.hooksPathto the correct Husky v9 value (.husky)git config core.hookspath— should output.husky.claude/docs/ENGINEERING.mdorCLAUDE.mdthatnpm cimust be run after cloning / cleaning the repo to activate hooksImplementation Plan
Overview
Hooks are dead in this checkout because
node_modules/huskyis not installed, so thepreparescript never regenerated.husky/_— Git'score.hooksPath=.husky/_then points at a missing directory and Git silently skips all hooks. The fix is to make hook activation reliable and self-documenting, not just re-runprepareonce: the real failure mode is that hooks depend on per-clone.git/config+ a regenerated, untracked.husky/_, which fresh clones and agent worktrees don't reliably have. We reactivate hooks, add a fast self-check that surfaces a broken/missing hook setup, and document the activation requirement where worktree/clone setup is already covered.Correction to the ticket's diagnosis (do not implement the ticket's step 1–2 verbatim): Husky v9 intentionally sets
core.hooksPathto.husky/_(the_directory is v9's own bootstrap dir holdinghusky.shand per-hook wrapper scripts that source the real.husky/<hook>). So.husky/_is the correct v9 value — it is not a v8 artifact, andpreparewill not change it to.husky. The bug is solely that.husky/_was never (re)generated in this checkout because husky isn't installed. Verification must therefore expect.husky/_, not.husky.Changes
package.json— add ahooks:checkscript that verifiescore.hooksPathresolves to an existing directory containing the bootstrap, so a dead hook setup is detectable (and runnable in pre-push). No dependency changes.CLAUDE.md— under the existing Worktrees & local dev section, state thatnpm ci(which runsprepare) is what activates Git hooks, that.husky/_is regenerated and untracked, and how to re-activate if hooks stop firing..husky/_by runningnpm ci/npm run prepareso hooks fire again on this machine and in each worktree.No source/schema/server-action/UI changes — this is a tooling/config + docs ticket. ENGINEERING §1–8 (actions, data, UX states, a11y) do not apply.
Implementation
npm ci(ornpm run prepareifnode_modulesis already present) to install husky and regenerate.husky/_; this is what was missing. Do not hand-edit.git/configor hand-create.husky/_.git config core.hookspathoutputs.husky/_(the correct Husky v9 value) and that.husky/_/now exists withhusky.shplus acommit-msgwrapper — correcting the ticket's expectation of.husky.hooks:checkscript topackage.jsonthat fails loudly whencore.hooksPathis unset or points at a missing directory (so the dead-hook state is caught instead of silently passing). Implement it as a small POSIX shell one-liner invoked viash -c(cross-platform with Git Bash; matches the existingcommit-msgshell-based hook), checking that the resolved hooks dir exists and contains the husky bootstrap.CLAUDE.mdWorktrees & local dev: note thatnpm cirunsprepareand that is what activates hooks;.husky/_is generated and intentionally untracked (self-ignored by husky's own.husky/_/.gitignore); each worktree needs its ownnpm cifor hooks to fire there; if hooks stop firing, re-runnpm ci/npm run prepareand verify withnpm run hooks:check. Keep it to 1–2 bullets in the existing list — do not restate husky internals.npm run prettier:check,npm run eslint:check,npm run tsc:check) — onlypackage.jsonandCLAUDE.mdchange, but the format/lint gates still apply.commit-msghook.Testing
npm ci, rungit config core.hookspath→ outputs.husky/_.npm run hooks:check→ exits 0 with hooks active; then temporarily set a bad path (git config core.hooksPath .husky/missing) and confirmnpm run hooks:checkexits non-zero; restore withgit config core.hooksPath .husky/_.Bad message) → blocked bycommit-msgwith the rule violation output.#292 ...lowercase, under 80 chars, no trailing period) → succeeds.Merge ...subject and abump version to vX.Y.Zsubject commit through without rejection.npm ci, confirmnpm run hooks:checkreports the broken state (documents the worktree requirement).Risks / notes
.huskyvs.husky/_) — follow this plan's corrected verification. Setting the path to.huskymanually would break the v9 bootstrap.core.hooksPathlives in per-clone.git/configand is not shared across clones/worktrees — there is no purely-committed way to force it; activation will always depend onnpm ci/preparerunning per checkout.hooks:check+ the doc note are the mitigation, not a hard guarantee.da12ea7,49958bf) are historical and are not rewritten here.