Chapter 8 of 11. Prerequisites: Chapter 3 for the commit discipline this layer sits underneath, and Chapter 5 for the remote gates this chapter's local layer runs before. Everything so far has been about GitHub: branches, pull requests, Actions. This chapter is about the one enforcement layer that runs before any of that exists, on your own machine, on
git commit.
Every gate covered so far in this course lives on GitHub's servers. A required check runs in Actions. A branch protection rule blocks a merge. Both of those happen after you've pushed, which means both of them happen after the mistake is already sitting in a commit somewhere. Git hooks are the layer underneath: code that runs on your machine, on your keystroke, before the commit object even exists. Get the policy right there and a whole class of mistake never reaches GitHub to be caught.
This chapter is about a specific, small hook constellation that runs on this machine for every repository, what it actually checks, what it doesn't, and an audit that caught the gap between the two.
Below GitHub: the layer that runs before the network call
Per Git's own hooks documentation, the pre-commit hook is "invoked before obtaining the proposed commit log message and making a commit," and a non-zero exit status stops git commit from creating the commit at all. That timing is the whole point. A GitHub Actions check runs against a commit that already exists, on a push that already happened. A pre-commit hook runs before the commit exists, offline, with no network round trip and no CI minutes spent. If the hook catches the mistake, there is nothing to revert, nothing to force-push over, and no record of the mistake ever entering history.
That also means a pre-commit hook is a weaker guarantee than a required GitHub check. It only runs if it's installed, only runs if it's executable, and can be skipped outright. Git documents that pre-commit and commit-msg hooks are bypassable with git commit --no-verify; pre-merge-commit is invoked by git merge, not git commit, and is bypassed with git merge --no-verify instead. So the two layers do different jobs: hooks stop the obvious stuff cheaply and locally, GitHub's checks are the backstop that can't be skipped by forgetting a flag on your own laptop. You want both. This chapter is only about the first one.
One hooks repo, every project: core.hooksPath
The default hooks directory, per the same documentation, is $GIT_DIR/hooks, meaning each repository's .git/hooks folder. That folder isn't tracked by git itself, so a hook dropped there rots the moment you clone the repo somewhere else, and copying the same hook into every project by hand is exactly the kind of manual step that gets skipped under deadline pressure.
The fix is the core.hooksPath configuration variable, which git documents as redirecting hook lookup away from $GIT_DIR/hooks to wherever you point it. On this machine that's a single dedicated repository, cloned once, with global config pointed at it:
git clone https://github.com/<owner>/git-hooks.git ~/.git-hooks
git config --global core.hooksPath ~/.git-hooks
Every repository on the machine now shares one pre-commit script. Change the policy once, in one repo, and it applies everywhere the next time a commit runs. That's the whole argument for pulling hooks out of .git/hooks and into their own versioned repo: policy as code means the policy is code, with a diff history, not a folder of scripts nobody remembers writing.
What the nameleak hook actually checks (and what it doesn't)
The constellation's oldest and most-used check exists for one specific, named reason. A past pull request let a real macOS username leak into a published article, inside a plist example that got copied verbatim from a local terminal session. The hook that resulted from that incident, and its header comment says so directly, blocks exactly two patterns: the machine's real username, and the operator's real name. Nothing more.
That specificity matters more than it looks. A hook named "nameleak" that blocks identifiers is not a secret scanner. It will not catch an API key, a database password, or a bearer token pasted into a commit. It was never built to. Writing that limitation into the hook's own header comment, rather than letting the hook's mere existence imply broader coverage, is the difference between a tool and a false sense of security. More on why that distinction earned its own audit finding in a moment.
The hook itself went through one important rewrite. The first version scanned the entire staged file content for the banned strings. That sounds more thorough, but it broke the moment anyone needed to write about the policy: editing CLAUDE.md or a devlog entry to document the exact patterns being blocked would trip the same check that catches an accidental leak, because the whole file, unchanged context included, got scanned every time. The result was bypass-token fatigue: the token got reached for constantly, for edits that had nothing to do with an actual leak, which is exactly how a bypass stops meaning anything.
The fix was to scan only what changed:
git diff --cached --unified=0 | grep -E '^\+' | grep -v '^\+\+\+'
--unified=0 strips context lines from the diff entirely, and filtering to lines starting with a single + (while dropping the +++ file-header line) leaves only newly added content. A file that already discusses the banned patterns, unchanged, never re-triggers the check. Only a genuinely new occurrence does. That one change moved the hook from "technically correct, practically annoying" to something people stopped needing to route around.
Fail open, and bypass as a documented token
Two design choices in this constellation are easy to miss and both are deliberate.
First, the bbpark-lint half of the same pre-commit script (a separate check, unrelated to the identifier scan, that lints backlog-agent metadata) fails open if its own lint script isn't present on the machine. A fresh checkout, on a machine that hasn't installed the linter yet, doesn't get blocked by a check it has no way to run. A check that hard-fails on missing tooling becomes, in practice, a check everyone learns to route around permanently, because the first person to hit it on a new machine reaches for --no-verify and never looks back.
Second, and this is the more interesting choice: bypass isn't left to git's own generic --no-verify flag. Each check defines its own named environment variable, BBBRAIN_NAMELEAK_BYPASS=1 for the identifier scan, a separate token for the roadmap check covered next. Git's docs confirm --no-verify is the built-in bypass, but it's silent about why someone skipped the check. A named token, set inline in the commit command and documented in the project's CLAUDE.md, records the specific policy being overridden in the shell history and in any CI log that captures the environment. git commit --no-verify -m "..." tells you a human decided to skip validation. BBBRAIN_NAMELEAK_BYPASS=1 git commit -m "..." tells you which validation, and points back at the documentation explaining when that's acceptable.
Roadmap ticks: a discipline enforced through the commit itself
The same hooks repo carries a second check with a different job: tying commit discipline to a project's own living documentation. Any commit whose subject line carries a phase identifier (or a Phase: X trailer) must also modify that project's docs/roadmap.md in the same commit. A commit that claims to close out planned work should leave a trace in the plan, not just in the diff. The hook warns rather than blocks for a 30-day grace window on a newly-installed machine, then starts blocking, with its own named bypass token for the rare legitimate exception.
This is the cleanest example in the whole constellation of what "policy as code" actually buys you over a habit or a house rule. A written convention that says "update the roadmap when you close a phase" is a sentence someone reads once and forgets under a deadline. A hook that inspects the commit subject and the staged diff enforces the same sentence every single time, without needing anyone to remember it.
The devlog gap: naming what isn't enforced
It would be tidy to say this constellation also enforces a devlog entry alongside every meaningful commit. It doesn't, and that gap is worth stating as plainly as the roadmap check's coverage. Devlog entries on this project are carried by a separate workflow (a structured propose-draft-ship pipeline for turning devlog entries into articles) and by the discipline of writing one, not by anything git commit inspects. No hook currently reads a diff and asks "did this change get narrated anywhere."
Naming that absence here, rather than letting the chapter imply a tidier system than exists, is not a throwaway caveat. It's the exact discipline the next section is about, applied to this chapter before that section makes the case for it.
The audit that caught a hook that was never installed
On 2026-07-25 a twelve-agent audit ran across this project's documentation and code, five parallel fact-checkers each comparing a different class of claim (philosophy against source docs, library one-liners against framework docs, stated data-handling behaviour against the actual backend code, entity leaks, and work-history claims against the repos they described) with an adversarial judge weighing in on every finding before it counted. Seven findings were upheld. All seven, which is the audit working as intended rather than failing to find problems.
One of those seven is the reason this chapter exists in this shape. The project's incident-response documentation, its README, and its own CLAUDE.md all described, in some form, a "pre-commit secret-leak hook verified by a deliberate canary commit." No such hook was ever installed. The active hook, the one this chapter has spent most of its length on, is the identifier-leak scrubber: two named patterns, diff-only, built in response to one specific incident. Gitleaks, the general-purpose secret scanner the documentation implied was running, had never been added to the repository at all.
Those are not the same claim wearing different words. A name-leak hook and a secret-leak hook have entirely different threat models. One catches a specific personal identifier appearing in new diff lines. The other, properly configured, catches API keys, tokens, and credential patterns across a much broader surface. Telling a future collaborator, or your own future self re-reading the incident-response plan under pressure, that the second one exists and is canary-verified when only the first is installed is worse than saying nothing. It manufactures confidence that the wrong gap is covered.
The fix the audit produced was not to quietly delete the inaccurate sentences. It was to correct the documentation everywhere the claim appeared to name what actually exists (the identifier scrubber), and to log the honest, still-open action item explicitly: install gitleaks, run it with --staged, and prove it with an actual canary commit, rather than describing that verification as already done.
What to install first
If you're building your own constellation from nothing, the order that earned its keep here is: start with one narrow, honestly-named check for the specific mistake that has actually happened to you (not a generic scanner you haven't verified), put it in its own repo from day one and wire it up with core.hooksPath so it isn't a one-off, scan only the diff rather than the whole file so the check doesn't punish people for documenting the policy it enforces, give it a named bypass token instead of leaning on --no-verify, and write the header comment that says exactly what it checks and why, in language specific enough that nobody could mistake it for a broader guarantee. Add the roadmap-style tie between a commit and your project's own documentation once that first check is solid. Leave the gaps you haven't closed yet written down as gaps, not silently implied as closed.
Install a global hooks repo with one honest check, then audit your own docs against it
Stand up your own core.hooksPath repository with a single pre-commit check for a mistake that has genuinely happened to you (or a placeholder pattern if it hasn't yet), scanning only the staged diff, with a named bypass token and a header comment that states its exact scope. Then grep your own docs for the word 'hook' and check every claim against what the script actually does.
Expected behaviour
- A dedicated git repo (not a folder inside any single project's .git/hooks) contains an executable pre-commit script, and git config --global core.hooksPath points at it
- The check scans git diff --cached --unified=0 filtered to added lines only, with a named environment variable (not --no-verify) as its bypass, documented in a comment in the script itself
- The script's header comment states plainly which specific pattern(s) it blocks and does not imply coverage of anything broader
- Every 'hook' claim in your own README, CLAUDE.md, or incident-response notes names a script that actually exists; any gap you find gets written down as an open action item, not silently deleted
PROVE IT Stage a file containing the banned pattern in a real project using the hooks repo, run git commit, and show it aborting before the commit is created; then rerun with the bypass token set and show it succeeding.
Your identifier-leak hook originally scanned the whole staged file for banned patterns, and got rewritten to scan only added diff lines. What problem did that rewrite actually solve?
Which git configuration variable redirects hook lookup from a project's own .git/hooks to a single shared repository used across every project on a machine?
A twelve-agent audit found documentation across a project's README, CLAUDE.md, and incident-response plan all describing a 'pre-commit secret-leak hook verified by a deliberate canary commit,' when the only installed hook was an identifier-leak scrubber blocking a username and a real name. Why is this specific kind of drift worse than simply having no secret-leak hook at all, and what did the fix actually do?
Show answer
A name-leak hook and a secret-leak hook cover entirely different threat models: one blocks a specific personal identifier appearing in new diff lines, the other (properly built) would catch API keys, tokens, and credentials across a much broader surface. Documentation claiming the second exists and is canary-verified, when only the first is installed, manufactures false confidence in exactly the gap that matters, which is more dangerous than an honest absence because nobody goes looking for a problem they believe is already solved. The fix corrected every surface making the claim to name the actual mechanism (the identifier scrubber) and logged the still-open action item explicitly, installing and canary-verifying a real secret scanner, rather than quietly deleting the inaccurate sentences.