1. Philosophy¶
Never stage files in a pre-commit hook that the user didn’t already explicitly git add.
Git’s staging area reflects the developer’s intentional choice about what belongs in the next commit. Your pre-commit hook must respect that intent—not override it.
✅ What you can do¶
(e.g., auto-formatting a file the user added.)
Apply safe, deterministic, non-semantic changes: formatting, lint fixes, license headers.
Use established tooling like
pre-commit—it handles partial staging correctly by stashing unstaged changes, running hooks, then restoring them.
❌ What you must not do¶
Stage new files (e.g., build artifacts, config files, logs) that weren’t already in the index.
Silently add files the user never touched or reviewed.
Mix unstaged and staged changes by formatting a file only partially added (unless you handle it like
pre-commitdoes).
⚠️ Why it matters¶
Auto-staging unknown files risks committing secrets, temp files, or unintended changes.
It breaks trust: developers expect
git committo include only what they staged.Debugging becomes harder when commits contain “mystery” changes.
🔧 Better alternatives¶
If a file must be up to date (e.g.,
package-lock.json), fail the commit and ask the user to run a script or add it manually.Enforce correctness in CI, not by silently altering the commit contents.
Use wrapper scripts (e.g.,
./commit.sh) for workflows that require pre-staging—keep automation explicit.
Golden Rule: A
pre-commithook may refine what’s staged—but never expand it. Preserve user agency. Respect the index.
2. Installation and Configuration¶
We use a Python package pre-commit for handling local hooks (see “ADR 26002: Adoption of the Pre-commit Framework”).
The .pre-commit-config.yaml file is already configured in the repo, so the developer needs to install pre-commit and install the config to .git/hooks repo:
$ uv add pre-commit
$ uv run pre-commit install
$ uv run pre-commit install --hook-type commit-msg
$ uv run pre-commit install --hook-type post-commitls ../../../.git/hooks | grep -E '^(pre-commit|commit-msg|post-commit)$'pre-commit
Run the repo configuration script, it will automatically handle all hook types.
3. Testing¶
$ uv run pre-commit [<hook_id>] [--all-files]4. Commit Message Validation¶
Commit message validation runs at the commit-msg stage — not pre-commit (which fires before the message is written) and not post-commit (which cannot abort the commit). The commit-msg hook receives the draft message and can reject it with a non-zero exit code, aborting the commit before it enters history. This makes it the only stage where message structure can be enforced as a blocking gate. See A-26004: Hook Stage Assignment Rationale for the full stage analysis.
The validate_commit_msg.py script runs as a commit-msg stage pre-commit hook, enforcing both the Conventional Commits subject format (Tier 2) and the Structured Commit Body convention.
What it validates:
| Check | Severity | Description |
|---|---|---|
| Subject format | Hard fail | Must match <type>[(<scope>)]: <description> with a valid type prefix (feat:, fix:, docs:, etc.) |
| Body bullets | Hard fail | Body must contain at least one line starting with - (a changelog bullet) |
| ArchTag presence | Hard fail | For refactor:, perf:, or BREAKING CHANGE commits, ArchTag:TAG-NAME must be present (Tier 3) |
With Squash-and-Merge, intermediate commit messages during development are less critical — the squash commit message (derived from the PR description) is the one that must pass validation. However, the hook runs on all local commits to build good habits and catch formatting issues early.
5. Changelog Preview (Post-Commit)¶
After each commit, the changelog-preview hook runs generate_changelog.py --verbose on the just-created commit. This shows the developer:
The CHANGELOG entry their commit will produce
Any bullets or subjects that will be filtered by exclusion patterns (e.g.,
CLAUDE.md,misc/)
This is informational only — it never blocks the commit. If the output looks incorrect, use git commit --amend to fix the commit message.