Git Course 0%

Hooks and pre-commit

Expert Git CLI ≈ 12 min

Before this lesson

What you will learn

  • Which hooks exist and when each runs
  • How to write one, and how to share hooks with a team
  • Why a hook is a convenience rather than a control

After this lesson you can

  • I can add a check that runs before every commit, and I know its limits

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:

Terminal
$ ls .git/hooks
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
pre-commit.sample
pre-merge-commit.sample
pre-push.sample

Remove 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:

.git/hooks/pre-commit
#!/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
Terminal
$ chmod +x .git/hooks/pre-commit

Then:

Terminal
$ git add config.env
$ git commit -m "chore: add config"
pre-commit: refusing to commit what looks like a credential

The 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

Terminal
$ 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:

.pre-commit-config.yaml
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: markdownlint

Each 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

Terminal
$ 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 folder

Common mistakes

  • Treating a hook as a control. --no-verify skips 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.hooksPath or 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.

  1. In your practice repository, create .git/hooks/pre-commit with the credential check from this lesson, and chmod +x it.
  2. Try to commit a file containing API_KEY=abc123 and read the refusal.
  3. Commit something clean and confirm the hook is invisible.
  4. Now commit the credential file with --no-verify and watch it succeed. Then remove that commit.
  5. Convert the same check into a .pre-commit-config.yaml entry, 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.

Check yourself

1. Why is a hook not a security control?
2. What should a pre-commit hook inspect?
3. Why do hooks not arrive with a clone?

Key terms

Repository (repo) Continuous integration (CI) Configuration