In Git, what are the post-checkout, post-merge and post-commit hooks typically used for?
answer
- They run after the state already changed
- Reacting, not gating
- One receives a branch-versus-file flag
- One is skipped when the merge conflicts
- One's exit status still colours the command
basics
~20 sThey are notification and reaction hooks that run after the work is done: reinstalling dependencies when a lockfile changed, regenerating build metadata, or pinging a local tool. None of them can reject or undo the commit, merge or checkout that triggered them.
solid answer
~50 sThe `post-` hooks fire once the repository state has already changed, so they exist to *react*, not to gate. `post-checkout` runs after `git checkout`, `git switch`, `git clone` and `git worktree add`, and receives the previous HEAD, the new HEAD and a flag that is 1 for a branch checkout and 0 for a file checkout — the standard use is "the lockfile changed between these two commits, so reinstall dependencies". `post-merge` runs after a successful merge (including the merge inside `git pull`) with a single argument saying whether it was a squash merge, and is used for the same dependency-refresh purpose. `post-commit` takes no arguments and is for local notification. Git documents that `post-commit` and `post-merge` cannot affect the outcome of their commands; `post-merge` is not run at all when the merge failed with conflicts. `post-checkout` is the odd one out: it also cannot change the outcome, but its exit status becomes the exit status of the checkout command.
code
bash · 7 lines#!/bin/sh
# .git/hooks/post-checkout — $1 old HEAD, $2 new HEAD, $3 1=branch 0=file
[ "$3" = "1" ] || exit 0
if git diff --name-only "$1" "$2" | grep -q '^package-lock\.json$'; then
echo "post-checkout: lockfile changed, dependencies may be stale"
fi
exit 0go deeper
Remember the simple split: hooks named post- run after the fact and cannot stop anything. Naming one realistic use, like refreshing dependencies after a branch switch, is enough.
Explain the arguments each hook receives, that post-merge is skipped on conflicts, and the post-checkout exit-status quirk. Be able to say why a check belongs in a pre- hook instead.
Show operational care: idempotent actions, early exit when nothing relevant changed, no surprise side effects, and awareness that a slow post-checkout hook discourages the cheap branching Git is built around.
Own the boundary between automation that helps and automation that surprises — decide what the repository is allowed to do to a developer's machine automatically, and where that belongs instead.
## Reaction hooks, not gates Every hook whose name begins with `post-` runs after Git has already changed the repository. The commit object exists, the merge is recorded, the working tree has been swapped. Git documents these hooks as unable to affect the outcome of their commands, which means the only thing they can usefully do is bring the *environment around* the repository back into agreement with the new state. That single sentence explains almost every legitimate use: your dependencies, generated files, database schema, IDE indexes and local caches are derived from the tree, and a checkout or merge can invalidate them silently. ## post-checkout Git invokes `post-checkout` with three arguments: the ref of the previous HEAD, the ref of the new HEAD, and a flag — `1` when the checkout changed branches, `0` when it was a file checkout (restoring paths from the index). It also runs after `git clone` (unless the clone was told not to check out) and after `git worktree add`. The flag matters. A hook that reinstalls dependencies on every `0` invocation will fire when someone restores a single file, which is pointless and slow. The idiomatic shape is: bail out unless the flag is `1`, then diff the two HEADs for the files you care about — `git diff --name-only $1 $2 -- package-lock.json` — and act only if something relevant moved. The documented wrinkle: this hook cannot change the outcome of `git switch`/`git checkout`, *other than* that the hook's exit status becomes the exit status of those commands. The branch switch still happened; the command merely reports failure. That confuses scripts that check `$?`, so exiting non-zero here is rarely a good idea. ## post-merge `post-merge` is invoked by `git merge` after a successful merge, which includes the merge half of `git pull`. It takes one argument, a status flag indicating whether the merge was a squash merge. Two properties are worth stating out loud: - it **is not executed when the merge failed because of conflicts**, so you cannot use it as a "after every pull" hook; - it cannot affect the merge outcome. The classic pairing is `post-checkout` plus `post-merge` running the same small script, because those are the two ways someone else's dependency change lands in your tree. ## post-commit `post-commit` takes no parameters and runs after the commit object has been created. Git's own documentation says it is meant primarily for notification. Realistic uses: bumping a local time-tracking tool, refreshing a status line, kicking off a background build. Because the commit already exists, a hook that "rejects" here can only do something destructive like resetting — which is a bad idea, because the correct place to refuse a commit is `pre-commit` or `commit-msg`. ## Practical cautions **Idempotence.** These hooks can fire in bursts — a pull that fast-forwards, then a branch switch, then another. Make the action cheap to repeat and no-op when nothing relevant changed. **No recursion.** A `post-commit` hook that itself commits will re-enter the hook. Guard it or, better, do not commit from a hook. **Speed.** A `post-checkout` hook that installs dependencies synchronously turns branch switching into a chore, and people start avoiding branch switching — the opposite of what cheap branching is for. Detect the no-change case first and exit immediately. **Surprise.** These hooks run automatically and can modify files that are not tracked, or start processes. Anything with a side effect the developer did not ask for should announce itself in one line of output rather than working silently.
- Why can't you use post-merge as a reliable after-every-pull hook?Because Git does not run `post-merge` when the merge failed due to conflicts, and a pull that fast-forwards or that only fetches will not necessarily reach it either. If you need to react to a conflicted state you have to notice it in the command's own output or exit status; the hook only fires on a merge that completed.
- What is odd about the exit status of post-checkout?It cannot change the outcome — the working tree is already updated — but Git makes the hook's exit status the exit status of `git checkout` or `git switch`. So a hook that returns non-zero leaves you on the new branch while the command reports failure, which breaks any script testing the return code. Exit 0 unless you truly want that signal.
- Why should a post-checkout hook check its third argument before doing any work?Because the flag distinguishes a branch checkout (1) from a file checkout (0). Restoring one file from the index does not change your dependency graph, so a hook that reinstalls packages on every invocation wastes seconds every time someone runs `git checkout -- path`. Return early on 0.
saying these in an interview costs you the question
- Trying to reject a commit from post-commit
- Expecting post-merge to run when the merge conflicts
- Ignoring the branch-versus-file flag in post-checkout
- Committing from inside a post-commit hook
- Assuming post-checkout can cancel the branch switch