Skip to content

Fix Husky hooks not enforced — core.hooksPath points to missing .husky/_ #292

Description

@b-at-neu

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

  1. Run npm run prepare to regenerate .husky/_ and update core.hooksPath to the correct Husky v9 value (.husky)
  2. Verify with git config core.hookspath — should output .husky
  3. Test by attempting a commit with a bad message and confirming it is blocked
  4. 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

  • Run npm ci (or npm run prepare if node_modules is already present) to install husky and regenerate .husky/_; this is what was missing. Do not hand-edit .git/config or hand-create .husky/_.
  • Confirm git config core.hookspath outputs .husky/_ (the correct Husky v9 value) and that .husky/_/ now exists with husky.sh plus a commit-msg wrapper — correcting the ticket's expectation of .husky.
  • Add a hooks:check script to package.json that fails loudly when core.hooksPath is 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 via sh -c (cross-platform with Git Bash; matches the existing commit-msg shell-based hook), checking that the resolved hooks dir exists and contains the husky bootstrap.
  • Update CLAUDE.md Worktrees & local dev: note that npm ci runs prepare and that is what activates hooks; .husky/_ is generated and intentionally untracked (self-ignored by husky's own .husky/_/.gitignore); each worktree needs its own npm ci for hooks to fire there; if hooks stop firing, re-run npm ci / npm run prepare and verify with npm run hooks:check. Keep it to 1–2 bullets in the existing list — do not restate husky internals.
  • Run the pre-push checks (npm run prettier:check, npm run eslint:check, npm run tsc:check) — only package.json and CLAUDE.md change, but the format/lint gates still apply.
  • Verify the commit guard end-to-end (see Testing) before opening the PR; the PR's own commit must pass the now-live commit-msg hook.

Testing

  • After npm ci, run git config core.hookspath → outputs .husky/_.
  • Run npm run hooks:check → exits 0 with hooks active; then temporarily set a bad path (git config core.hooksPath .husky/missing) and confirm npm run hooks:check exits non-zero; restore with git config core.hooksPath .husky/_.
  • Attempt a commit with a bad subject (e.g. Bad message) → blocked by commit-msg with the rule violation output.
  • Attempt a commit with a valid subject (#292 ... lowercase, under 80 chars, no trailing period) → succeeds.
  • Confirm exemptions still pass: a Merge ... subject and a bump version to vX.Y.Z subject commit through without rejection.
  • In a second worktree without npm ci, confirm npm run hooks:check reports the broken state (documents the worktree requirement).

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.

Metadata

Metadata

Assignees

Labels

claudeWill be worked on by Claudeplan reviewPlan written, awaiting human approval in cockpit

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions