- decisions/0007-pre-commit-hooks-and-conventional-commits.md formalizes Husky + lint-staged + commitlint with Conventional Commits as the local quality-gate baseline. - decisions/README.md index updated. - docs/setup/03 section 8 rewritten to reference the ADR and document the full hook setup (pre-commit, commit-msg, commitlint config). - docs/setup/03 future-work table 'ADR(s)' column removed; future ADR numbers are now assigned at the moment each ADR is written, not pre-reserved. - CLAUDE.md aligned: pre-allocated phase-2 ADR numbers replaced by phase references; a pointer to ADR-0007 added under 'Local quality gates'.
5.4 KiB
status, date, decision-makers, tags
| status | date | decision-makers | tags | |
|---|---|---|---|---|
| accepted | 2026-04-29 | R&D Lead |
|
Pre-commit hooks and Conventional Commits
Context and Problem Statement
Without local enforcement, trivial issues — unformatted code, lint errors, ill-formed commit messages — only surface in CI. Each round-trip costs minutes of feedback latency and pollutes the commit history with fixup noise. We need a lightweight, well-known mechanism that catches these issues at commit time and enforces a consistent commit message format, while staying fast enough not to discourage frequent commits.
Which tooling do we adopt for git pre-commit checks and commit message validation?
Decision Drivers
- Fast feedback: catch trivial issues locally; CI is defense in depth, not the first line.
- Consistency of code style and commit history across contributors.
- Conventional commit history that machines can read (later automation: changelog, release notes, semver bumps).
- Mainstream tooling — no exotic shell hooks, no single-maintainer projects.
- Easy onboarding: no extra runtime to manage beyond Node/pnpm.
Considered Options
- Husky + lint-staged + commitlint with Conventional Commits. (Chosen.)
- No git hooks — rely on CI.
pre-commit(Python framework).lefthook(Go single-binary hook manager).- Custom shell scripts in
.git/hooks/.
Decision Outcome
Chosen option: Husky + lint-staged + commitlint, with Conventional Commits as the commit-message specification.
Hooks installed under .husky/:
| Hook | Action |
|---|---|
pre-commit |
pnpm exec lint-staged — runs lint and format on staged files only |
commit-msg |
pnpm exec commitlint --edit "$1" — validates the commit message against Conventional Commits |
Configuration:
package.json:"prepare": "husky"script (Husky 9+ pattern),lint-stagedconfig block.commitlint.config.cjs:module.exports = { extends: ['@commitlint/config-conventional'] }.
Scope of lint-staged: only fast checks on staged files (lint, format). Type-check and tests stay in pnpm nx affected -t lint test build in CI — running the full graph on every commit would slow commits unacceptably and discourage frequent commits.
CI re-runs the same checks (defense in depth — hooks can be bypassed with --no-verify).
Consequences
- Good, because trivial issues are caught locally and don't pollute PR history.
- Good, because Conventional Commits gives us a machine-readable history — enables automated changelogs, semver inference, and release tooling without retroactive cleanup.
- Good, because Husky / lint-staged / commitlint are the de facto standard in JS/TS projects — wide community, abundant documentation, low surprise factor.
- Good, because lint-staged keeps commit time low (only changed files are checked).
- Bad, because hooks can be bypassed (
git commit --no-verify); CI must remain authoritative. - Bad, because Husky 9 changed its installation pattern (no
husky install, justhusky); contributors with stale instructions can be confused. Mitigated by the setup guide. - Bad, because Conventional Commits adds a small learning curve; mitigated by IDE plugins,
commitlinterror messages, and a one-page contributor cheatsheet (future doc).
Confirmation
package.jsondeclareshusky,lint-staged,@commitlint/cli,@commitlint/config-conventionalasdevDependencies.package.jsonhas a"prepare": "husky"script..husky/pre-commitrunspnpm exec lint-stagedand is executable..husky/commit-msgrunspnpm exec commitlint --edit "$1"and is executable.commitlint.config.cjsexists at the workspace root and extends@commitlint/config-conventional.- CI pipeline (future ADR) re-runs lint and format checks on every push, plus a Conventional Commits validation on the PR commit range.
Pros and Cons of the Options
Husky + lint-staged + commitlint (chosen)
- Good, because the trio is the de facto standard in the JS/TS ecosystem.
- Good, because each tool is single-purpose and composable.
- Good, because the configuration lives with the repo and is versioned with the code.
- Bad, because three packages instead of one — slightly more dependency surface.
No git hooks
- Good, because zero local setup.
- Bad, because every trivial issue costs a CI round-trip. Wasteful and noisy.
pre-commit (Python)
- Good, because language-agnostic, used in mixed-language repos (Python + JS + Go).
- Bad, because adds a Python runtime dependency to a Node-only project — extra setup for contributors, especially under WSL where Python toolchains are not always uniform.
lefthook
- Good, because single Go binary, very fast, parallel hook execution, declarative YAML config.
- Bad, because smaller community than Husky in JS/TS land; less prior art and fewer answers when something breaks.
- Status: kept on the watch list for re-evaluation if commit performance becomes a pain.
Custom shell scripts in .git/hooks/
- Good, because zero dependency.
- Bad, because hooks aren't versioned with the repo by default; a wrapper layer is needed anyway. Bricolage.
More Information
- Husky: https://typicode.github.io/husky/
- lint-staged: https://github.com/lint-staged/lint-staged
- commitlint: https://commitlint.js.org/
- Conventional Commits: https://www.conventionalcommits.org/
- Related: future quality / CI ADR will re-run these checks as defense in depth.