Hooks and pre-commit
Expert Git CLI
Why this matters
A hook catches a mistake a second after you make it, instead of ten minutes later in a pipeline. That is a genuine improvement to a working day, and it comes with one important limit: hooks run on your machine, and anyone can skip them.
What they are
Executable scripts in .git/hooks/, run by Git at defined moments. A fresh repository has samples:
$ ls .git/hooksapplypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
pre-commit.sample
pre-merge-commit.sample
pre-push.sampleRemove the .sample suffix, make it executable, and it runs. A non-zero exit code cancels the operation.
The ones worth knowing
| Hook | Runs | Typical use |
|---|---|---|
| pre-commit | Before the commit is created | Linting, formatting, scanning for secrets |
| commit-msg | After the message is written | Enforcing a message convention (lesson 13.8) |
| prepare-commit-msg | Before the editor opens | Pre-filling a template or an issue number |
| pre-push | Before a push | Running tests, blocking pushes to main |
| post-checkout, post-merge | After switching or merging | Reinstalling dependencies when a lock file changed |
Server-side hooks (pre-receive, update) exist too and are how a self-managed platform enforces rules centrally, but you cannot install them; that is what push rules and rulesets are for (lesson 9.5).
Writing one
A pre-commit hook that refuses anything that looks like a credential:
#!/bin/sh
if git diff --cached | grep -qE 'PASSWORD=|API_KEY='; then
echo "pre-commit: refusing to commit what looks like a credential"
exit 1
fi$ chmod +x .git/hooks/pre-commitThen:
$ git add config.env
$ git commit -m "chore: add config"pre-commit: refusing to commit what looks like a credentialThe commit does not happen. A clean commit proceeds silently, which is how a good hook behaves: invisible until it matters.
Note git diff --cached: the hook must inspect what is staged, not the working tree, or it will pass on files you have not staged and fail on ones you have not.
Why hooks are not security
$ git commit --no-verify -m "chore: add config"That skips every commit-stage hook, and it exists for legitimate reasons: a broken hook, an emergency, a commit of a file that legitimately matches a pattern. It also means:
A hook is a convenience for the person who installed it, not a control over anyone else.
The real controls live on the server: protected branches, push rules, required status checks and secret scanning (lesson 14.7). A sensible arrangement uses both: the hook catches the mistake in a second, and the pipeline catches it if the hook was skipped or absent.
Sharing hooks with a team
.git/hooks/ is not committed, so a hook you write is yours alone. Two ways round it:
| Approach | How |
|---|---|
core.hooksPath |
Commit a hooks/ folder and have everyone run git config core.hooksPath hooks |
| A framework | Commit a configuration file and let a tool install the hooks |
The framework almost everyone uses is pre-commit, which despite the name manages several hook stages. Its configuration is a committed file:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0
hooks:
- id: markdownlintEach contributor runs pre-commit install once, and from then on the same checks run for everyone, pinned to the same versions. That last point is what makes it better than hand-written scripts: everyone gets the same tool, not whatever is on their machine.
Keeping hooks pleasant
A hook that annoys people gets skipped, so three rules:
- Fast. Anything over a second or two on a normal commit will be bypassed.
- Only what changed. Lint the staged files, not the whole repository.
- Clear message. Say what failed and how to fix it, in the words the person needs.
How to do it
$ ls .git/hooks
$ cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit
$ git commit --no-verify -m "…" # skip them once
$ git config core.hooksPath hooks # use a committed folderHooks run whether the commit came from a button or a command, and their output appears in the Git output channel. A failing hook shows as a commit that did not happen, with the message in View → Output → Git (lesson 15.9).
The same; hook output appears in the Console tab. IntelliJ's Before Commit checks are the IDE's own equivalent and are not hooks (lesson 16.1).
Server-side equivalents are push rules: commit message patterns, file size limits, prohibited file names, and requiring signed commits. Self-managed instances can also install real server hooks.
Rulesets cover the same ground: commit message patterns, required signatures, restricted file paths and sizes.
Common mistakes
- Treating a hook as a control.
--no-verifyskips it. - Inspecting the working tree instead of
git diff --cached. - A slow hook, which trains everyone to bypass it.
- Expecting hooks to travel with a clone. They do not; use
core.hooksPathor a framework. - Forgetting
chmod +x, so the hook is silently ignored. - A hook that rewrites files during commit, which surprises people and can stage things they did not choose.
Try it yourself
Goal: write a hook, use it, and skip it.
- In your practice repository, create
.git/hooks/pre-commitwith the credential check from this lesson, andchmod +xit. - Try to commit a file containing
API_KEY=abc123and read the refusal. - Commit something clean and confirm the hook is invisible.
- Now commit the credential file with
--no-verifyand watch it succeed. Then remove that commit. - Convert the same check into a
.pre-commit-config.yamlentry, or write down why the pipeline is the place it really belongs.
Expected result: a working hook, one deliberate bypass, and a clear sense of why the server-side check is the one that counts.
Show solution
Step 4 is the lesson. The bypass is one flag and it exists for good reasons, which means a hook can never be the only place a rule is enforced. The pattern that works is a fast hook for your own benefit plus a pipeline check for the team's.