Git hooks: what they are and which ones exist
A git hook is a program git runs at a fixed point of an operation: before a commit is created, after a push lands on a server, after a branch is checked out. It is not a plugin and not a git command. It is an executable file with an agreed name in a folder git looks at, and its exit status is the answer git waits for: zero means continue, anything else stops the operation at the points where git allows a hook to stop it.
Hooks are how a repository enforces what its authors agreed on. The pre-commit hook refuses a commit that does not parse, the post-commit hook regenerates a file after the commit exists, and a pre-receive hook on the server refuses a push that breaks the same rules for everyone. How the two local hooks work as one pair, and what went wrong when they did not, is in pre-commit and post-commit together.
What a git hook is
Git does not interpret hooks. It looks for a file with a known name in the hooks directory, and if the file exists and is executable, it runs it with the arguments, the standard input and the environment that belong to that point of the operation. Everything else is the hook's business: bash, Python, PHP or a compiled binary. The name of the file decides when it runs; the exit status decides what git does next.
- The name is the trigger. pre-commit runs before a commit is created, post-merge after a merge. A file called pre-commit.sh is never run: the name must match exactly.
- The executable bit is the switch. A hook without +x is silently ignored. That is the first thing to check when a hook «does not work».
- Zero means continue. A non-zero status stops the operation where git allows a hook to stop it. After the fact — post-commit, post-receive — the status changes nothing.
- The context arrives three ways. Arguments (the path of the message file for commit-msg), standard input (the refs being pushed, for pre-push and pre-receive), and environment variables (GIT_DIR, GIT_INDEX_FILE, GIT_EDITOR).
- The working directory is the repository root for local hooks. The push hooks on the server run in $GIT_DIR, so a server hook cannot assume a checked-out tree.
- There is no timeout and no sandbox. A hook that waits blocks the command, and it runs with the rights of the person who started it.
Where git looks for hooks
The default directory is .git/hooks inside the repository. git init fills it with .sample files: every hook git ships an example for, none of them active. Renaming one and making it executable turns it on.
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 prepare-commit-msg.sample pre-push.sample pre-rebase.sample pre-receive.sample push-to-checkout.sample sendemail-validate.sample update.sample
The directory can be moved with core.hooksPath, which is how a repository keeps its hooks in a tracked folder instead of inside .git:
git config core.hooksPath .githooks
The setting lives in the local configuration, so a fresh clone still has to set it. A tracked folder plus one setup command is the smallest way to share hooks. A framework is the larger way: the pre-commit framework installs the two hook files that call it and keeps the checks themselves in a YAML file next to the code.
Turn one on, skip one, watch one
Three commands cover most of the daily use. The first makes a hook active. The second skips the hooks that guard the commit — pre-commit, pre-merge-commit and commit-msg — but not prepare-commit-msg, and not the post-* hooks, which still run. The third prints every hook git executes, with its path and arguments, which answers «did it run at all?» before any debugging of the hook itself.
chmod +x .git/hooks/pre-commit
git commit --no-verify -m "WIP"
GIT_TRACE=1 git commit -m "Test"
The three hook lines of a commit, with the timestamps cut off:
run-command.c:659 trace: run_command: GIT_EDITOR=: GIT_INDEX_FILE=.git/index .git/hooks/pre-commit run-command.c:659 trace: run_command: GIT_EDITOR=: GIT_INDEX_FILE=.git/index .git/hooks/commit-msg .git/COMMIT_EDITMSG run-command.c:659 trace: run_command: GIT_EDITOR=: GIT_INDEX_FILE=.git/index .git/hooks/post-commit
The hooks of Git 2.43
Git 2.43 knows 28 hooks. They are easier to read in the order git runs them than alphabetically, so the tables below follow one commit, one push and one piece of housekeeping.
Around a commit.
| Hook | When it runs | Can it stop the commit |
|---|---|---|
| pre-commit | on git commit, before the message is composed | yes, --no-verify skips it |
| pre-merge-commit | after a merge succeeded, before the merge commit | yes, --no-verify skips it |
| prepare-commit-msg | after the default message exists, before the editor opens | yes, and --no-verify does not skip it |
| commit-msg | after the message is written, gets its file as an argument | yes, --no-verify skips it |
| post-commit | after the commit object exists and HEAD moved | no |
| pre-rebase | before git rebase starts | yes, refuses the rebase |
Around the working tree and the refs.
| Hook | When it runs | Can it stop the command |
|---|---|---|
| post-checkout | after git checkout, git switch, git clone and git worktree add | no, its status becomes the command's status |
| post-merge | after a merge or a pull, not after a conflicted merge | no |
| post-rewrite | after git commit --amend and git rebase rewrite commits | no |
| reference-transaction | around every ref update: prepared, committed, aborted | yes, in the prepared state |
| post-index-change | after the index file is written | no |
Around patches and mail.
| Hook | When it runs | Can it stop the command |
|---|---|---|
| applypatch-msg | on git am, on the proposed message | yes |
| pre-applypatch | on git am, after the patch is applied, before the commit | yes |
| post-applypatch | on git am, after the commit | no |
| sendemail-validate | before git send-email sends a patch | yes |
| p4-changelist, p4-prepare-changelist, p4-post-changelist, p4-pre-submit | the Perforce bridge: edit, prepare, after submit, before submit | yes, except p4-post-changelist |
Around a push. One hook on the client, six on the server.
| Hook | Where it runs | What it can do |
|---|---|---|
| pre-push | client, before the objects are sent; the refs arrive on stdin | refuse the whole push |
| pre-receive | server, once per push, before any ref is updated | refuse every ref in the push |
| update | server, once per ref, before that ref is updated | refuse that one ref |
| proc-receive | server, when a ref is handled by a helper command | refuse or rewrite that ref |
| post-receive | server, after all refs are updated | notify; cannot refuse |
| post-update | server, after post-receive, gets the updated refs as arguments | notify; cannot refuse |
| push-to-checkout | server, when a push targets the branch checked out in a non-bare repository | refuse, or update the tree itself |
Housekeeping.
| Hook | When it runs | What it can do |
|---|---|---|
| pre-auto-gc | before the automatic garbage collection | postpone it |
| fsmonitor-watchman | the integration between the fsmonitor daemon and watchman | report changed paths |
| post-index-change | after the index file is written, with two flags as arguments | notify |
What a hook cannot do
- A post hook cannot undo anything. By the time post-commit runs, the commit exists. A failing post-commit hook does not fail git commit: the command still exits 0, and the only trace is the text the hook printed.
- A hook is not a security boundary. Client hooks live on the machine of the person committing, who can delete them, edit them or pass --no-verify. Only the server hooks — and a server that refuses force pushes and unsigned commits — bind everyone.
- A clone brings no hooks. .git/hooks is inside .git, which is not part of the repository. Every clone starts with the samples and nothing else.
- A pre-commit hook can change the commit. The commit is built from the index after the hook returns, so a hook that runs git add adds those files to the commit being made. That is how formatters commit their own output, and how a hook adds a file nobody meant to commit.
- A hook sees the index, not the working tree. Staged and unstaged versions of the same file are different content, and the hook is given the staged one. The pre-commit hook shows what that means in practice.
- Hooks are not portable by themselves. A bash hook on Linux is not the same file that runs on Windows without Git Bash, and a hook that calls php needs PHP on the machine that commits.
Client side and server side
The split that matters is not pre and post, it is local and remote. Everything up to pre-push runs on the machine that commits: fast, private, and optional. Everything from pre-receive runs on the server, after the objects have travelled, and cannot be skipped by the person pushing. A rule that must hold for every commit belongs on the server; a rule that saves the author a round trip belongs on the client. Most repositories run the same check in both places: the local hook for speed, the server hook for certainty.
The pre-commit framework
Writing the same shell checks in every repository gets old, so most projects use a framework that keeps the checks in a YAML file, installs the hook files and runs the tools in isolated environments. The pre-commit framework is the one this site uses. It installs both .git/hooks/pre-commit and .git/hooks/post-commit, and the checks are listed in .pre-commit-config.yaml at the repository root.
The framework adds one behaviour that plain hooks do not have, and it is the source of the incident described in the workflow article: before each stage it saves every unstaged change into ~/.cache/pre-commit/patch<timestamp>-<pid>, runs git checkout -- . so the checks see a clean worktree, and re-applies the patch afterwards. If that re-apply fails, the unstaged work is not lost — the patch file stays on disk — but it is no longer in the working tree.
Articles in this section
| The pre-commit hook: what it checks, what it sees and how to skip it | |
| The post-commit hook: reactions that cannot undo a commit | |
| pre-commit and post-commit together: a gate and a reaction |