What is a pre-commit hook?
A pre-commit hook is a script that Git runs at the start of git commit, before Git records the commit. It checks the staged changes, which are the edits selected with git add, and a common check is linting. A nonzero exit code stops the commit. The name also refers to pre-commit, an open-source hook manager.
Git's hooks documentation says that the pre-commit hook runs before Git gets the commit message, and that Git ignores a hook file that is not executable. The same page says that git commit --no-verify bypasses the hook.
A pre-commit hook is one of several Git hooks, which are scripts that run at set points in Git's workflow. Its checks usually finish in seconds, because the hook runs at every commit. A continuous integration and delivery (CI/CD) pipeline can run the same checks again on a server.
How does a pre-commit hook work?
Git runs a pre-commit hook in four steps:
- A developer stages changes with
git addand runsgit commit. - Git looks for an executable file named
pre-commitin the hooks folder, which is.git/hooksunlesscore.hooksPathnames another folder. - Git runs that file from the working tree's root, and the file checks the staged changes.
- Git reads the exit code. Code 0 lets the commit continue, and any other code stops it.
A hook can be 2 lines of shell. This one exits with a nonzero code when a staged line ends in whitespace:
#!/bin/sh
exec git diff --cached --check
Git does not copy hooks when someone clones a repository. A hook manager adds a committed config file and an install step that each clone needs. These open-source managers are common:
- pre-commit. The pre-commit tool reads
.pre-commit-config.yaml, andpre-commit installwrites a script to.git/hooks/pre-commit. Each hook runs in its own environment at a pinned version. - prek. The prek tool is a single Rust binary that reads pre-commit's config file and hooks and needs no Python.
- Husky. The Husky npm package sets
core.hooksPathso Git runs hook scripts committed in a.huskyfolder. - Lefthook. The Lefthook tool is a Go binary that reads
lefthook.ymland can run commands in parallel.
The pre-commit documentation says the tool stashes unstaged edits while hooks run, so the checks read only the staged contents. Without a commit, pre-commit run checks the staged files, and pre-commit run --all-files checks every tracked file. Git's git hook run pre-commit runs the installed script.
What is an example of a pre-commit hook?
Here is an illustrative example. Acme Co. sells furniture online. A developer at Acme is working on the request "Let customers edit their delivery address during checkout." The web store's repository has this .pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: detect-private-key
The developer runs pre-commit install --install-hooks once, which also sets up each hook's environment. After editing checkout/address.js, the developer runs git add ., which also stages a local key file by mistake. The next git commit starts the hook, which prints this output:
$ git commit -m "Let customers edit the delivery address"
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing checkout/address.js
fix end of files.........................................................Passed
detect private key.......................................................Failed
- hook id: detect-private-key
- exit code: 1
Private key found: config/payments.pem
Git records no commit. The whitespace hook already fixed the file, so the developer stages the fix, unstages the key file with git restore --staged config/payments.pem, adds it to .gitignore, and commits again. All three hooks pass, and Git records the commit. Because the first commit failed, the key never entered the repository's history. The key check is a basic form of secret scanning, and a CI check would run only after the key reached the server.
The hook read the text of the staged files and never started the store. The change also had a bug that only a running store shows. The address saved, but the cart emptied. This example is simplified. A real repository would also pin a linter for its language, e.g. ESLint for JavaScript.
What changes when a coding agent writes the code?
A coding agent commits by running git commit in a shell, so the pre-commit hook runs inside the agent's session. The hook's output returns to the agent as the result of that command, and the agent can edit the files and try again. A hook that fixes files, e.g. trailing-whitespace, still fails the commit. The agent has to stage the fixed files before it commits again, or the hook fails a second time.
The loop has two weak points. Git skips the hook when the command includes --no-verify or its short form -n. An agent can also weaken the check instead of fixing the code, e.g. by adding the failing file to an exclude pattern in .pre-commit-config.yaml. When a hook runs tests, editing a failing test instead of the code is test tampering.
A team can run the same hooks again in CI, where neither flag has any effect, and review each change to .pre-commit-config.yaml in the diff. Agent hooks add checks at fixed points in the agent's own loop, e.g. when the agent tries to end its turn.
What are the limits of pre-commit hooks?
A pre-commit hook gives fast feedback, but it has four limits:
- It is optional. A developer who never runs the install step has no hook, and a code host's web editor never runs one.
- It checks only the staged files. A change can break a file it did not touch, and the hook does not read that file.
- It reads code without running it. Most hook checks are static analysis, so they miss failures that only dynamic analysis finds, e.g. the emptied cart.
- It adds a wait to every commit. People skip a slow hook.
Checks that belong in the hook finish in seconds and read one file at a time:
- Formatting. A formatter rewrites each staged file's layout.
- Linting. A linter reports code that breaks a rule.
- Secrets. A secret check stops a key before it enters the history.
- File checks. A file check rejects merge conflict markers and oversized files.
Slower checks, e.g. a full run of the unit tests, usually run in CI or as local CI checks before a push. A passing hook is not the same as working software.
How is a pre-commit hook different from a CI check?
A pre-commit hook runs on the developer's machine before the commit exists, and a CI check runs on a server after a push. The hook reads the staged files in one working copy. The CI check usually starts from a fresh copy of the pushed code, so it can build the project and run slower tests. A code host can require a CI check to pass before a merge, but it cannot require a local hook.
Teams often run the same hooks in both places, e.g. with pre-commit run --all-files --show-diff-on-failure as a CI step, which prints the changes a failing hook made. A pre-push hook sits between the two, and the pre-commit, pre-push, and CI comparison shows which checks fit each place.
FAQs
How does pre-commit work?
The pre-commit tool works from a config file in the repository that lists hooks and pins the version of each hook repository. Its install command writes one script into the Git hooks folder. On each commit, the tool stashes unstaged edits, runs the listed hooks on the staged files, and stops the commit if any hook fails.
Do pre-commit hooks work with any Git repository?
Pre-commit hooks work in any Git repository where commits are made with Git on a local machine. Each clone needs its own install, because Git does not copy hooks when a repository is cloned. Commits made elsewhere, e.g. in a code host's web editor, never run the local hook.
How do you run pre-commit hooks manually?
Pre-commit hooks can run manually without a commit. With the pre-commit tool, pre-commit run checks the staged files, and the same command with the flag for all files checks every file that Git tracks. Git can also run the installed script with git hook run pre-commit.
How do you use pre-commit in CI?
Teams use pre-commit in CI by adding a pipeline step that runs every hook against all files. That step catches commits that skipped the local hook or came from a clone without it. An option that shows the diff on failure prints what a fixing hook changed, so the log shows what to correct.