skip to content

questions

5

In Git, what happens when a client-side hook such as pre-commit exits with a non-zero status?

level: juniorimportance: must knowfreq 62%

answer

  1. It is just a program Git runs
  2. Git only reads one thing from it
  3. Zero versus non-zero
  4. Names starting with post- are too late
  5. Rename the .sample file, chmod +x

basics

~10 s

A non-zero exit from a pre-operation hook such as pre-commit, commit-msg or pre-push aborts the operation — no commit is created, no push is sent. Post-hooks like post-commit run afterwards and cannot abort anything.

solid answer

~40 s

Git hooks are ordinary executables in the repository's hook directory (`.git/hooks` by default), named exactly after the hook point — `pre-commit`, `commit-msg`, `pre-push` — with no file extension and the executable bit set. Git runs the matching file at the right moment and reads its **exit status**: zero means proceed, non-zero means abort. So a failing `pre-commit` stops `git commit` before the message editor even opens, and a failing `pre-push` makes `git push` exit without contacting the remote at all. Anything the hook writes to stderr is shown to the developer, which is how a hook explains *why* it rejected the operation. Hooks whose names start with `post-` (`post-commit`, `post-merge`, `post-checkout`) run after the work is done and cannot undo it. Git ships `.sample` versions of the hooks, which are inert until renamed.

code

bash · 7 lines
bash
#!/bin/sh
# .git/hooks/pre-commit  (rename from pre-commit.sample, then chmod +x)
if git diff --cached --name-only --diff-filter=ACM | grep -q '^\.env$'; then
  echo "pre-commit: refusing to commit .env" >&2
  exit 1
fi
exit 0

go deeper

for a junior

Recall that a hook is an executable file in .git/hooks named after the event, and that exiting non-zero cancels the command. Be able to name pre-commit and commit-msg.

for a middle

Explain the exit-code contract precisely, which hooks run before versus after the state change, and why the samples are inert until renamed and made executable.

for a senior

Show the production judgment: hooks must be fast, must inspect the staged index rather than the working tree, and must print an actionable reason to stderr instead of failing silently.

for a principal

Frame hooks as developer feedback with no enforcement guarantee, and argue for where the authoritative check belongs so that a fast local check and a binding remote check do not drift apart.

## What a hook is A Git hook is not a plugin API or a config setting — it is just a program that Git executes at a fixed point in a command's lifecycle. Git looks in the repository's hook directory (by default `.git/hooks` inside a normal clone) for a file whose name is exactly the hook point, runs it, and inspects the process exit status. Any language works as long as the file is executable and, on Unix, starts with a shebang line such as `#!/bin/sh` or `#!/usr/bin/env python3`. ## Why the samples don't fire A fresh repository already contains files like `pre-commit.sample` and `commit-msg.sample`, copied from Git's template directory when the repository was created. They are deliberately inert: Git looks for `pre-commit`, not `pre-commit.sample`, so nothing happens until you rename the file and make sure it is executable (`chmod +x`). "I wrote the hook and nothing ran" is nearly always one of three things: wrong filename, missing executable bit, or a bad shebang. ## The exit-code contract The contract is deliberately primitive. - **Exit 0** — the hook approves; Git continues. - **Any non-zero exit** — for a hook that runs *before* the operation, Git aborts. `git commit` creates no commit object, `git push` transfers nothing. That is the whole protocol. There is no rich error object, no severity level, no "warn but continue". If a hook wants to warn without blocking, it prints a message and exits 0. ## Which hooks can actually abort The pre-operation hooks can veto: `pre-commit` (before the message is even collected), `prepare-commit-msg` (before the editor opens), `commit-msg` (after the message is written), and `pre-push` (before any objects go over the wire). The notification hooks cannot: `post-commit`, `post-merge` and `post-checkout` all run after the state change is complete, and Git documents that they cannot affect the outcome. `post-checkout` has one wrinkle — its exit status becomes the exit status of `git checkout`/`git switch`, so a failing hook makes the command *report* failure even though the checkout already happened. ## Talking to the developer Standard output and standard error from a client-side hook go to the terminal, so the human sees them. Practically that means: print the reason for rejection to stderr, keep it to a couple of lines, and name the file or rule that failed. A hook that exits 1 silently produces a mystifying `git commit` that appears to do nothing. ## Hooks run in the repository Git invokes the hook with the working tree as the current directory (for a normal, non-bare repository) and sets Git environment variables such as `GIT_DIR`. A subtlety that bites `pre-commit` hooks: the commit is built from the **index**, not from the working tree, so linting the files on disk can approve content that is not what will be committed. The careful pattern is to inspect staged content, for example `git diff --cached --name-only --diff-filter=ACM`. ## Bypassing `git commit --no-verify` skips `pre-commit` and `commit-msg`; `git push --no-verify` skips `pre-push`. Because the hook is a plain file inside `.git`, a developer can also just delete it or remove its executable bit. That is why client hooks are a fast feedback mechanism, not a guarantee. ## Performance Every pre-operation hook sits directly in the developer's inner loop. A `pre-commit` hook that runs the whole test suite turns a one-second commit into a coffee break and trains the team to type `--no-verify` reflexively. Keep client hooks to sub-second checks on the staged files and let slower verification happen elsewhere.

  • Why does a pre-commit hook that lints the files on disk sometimes approve a commit that is actually broken?
    Because the commit is built from the index, not the working tree. If a file is partially staged, or edited after staging, the on-disk version differs from what will be recorded. A correct hook inspects staged content — for example `git diff --cached --name-only` plus `git show :<path>` — rather than reading the working tree directly.
  • A colleague swears their new pre-commit hook never runs. What do you check first?
    Three things, in order: the filename is exactly `pre-commit` with no `.sample` suffix, the file is executable, and the shebang points at an interpreter that exists. Also confirm they are committing in the repository that holds the hook — hooks live per repository, and a submodule or a linked worktree has its own hook resolution.
  • Should a hook ever exit non-zero just to warn about style?
    No. Non-zero is a veto, and a veto for a cosmetic issue teaches the team to pass `--no-verify` habitually, which disables the checks that actually matter. Print the warning and exit 0, or fix the issue automatically and exit 0.

Think of a pre-operation hook as a bouncer at the door: the only thing the club hears from them is yes or no, and once you are inside, the bouncer at the exit (post-commit) can complain but cannot put you back outside.

saying these in an interview costs you the question

  • Thinking hooks are enforced by the server or by clone
  • Believing post-commit can undo or reject a commit
  • Assuming .sample files are active out of the box
  • Saying the hook returns a message Git parses, not an exit code
  • Linting the working tree instead of the staged index

context

open as a page

How do Git's pre-commit, prepare-commit-msg and commit-msg hooks differ during one git commit?

level: middleimportance: must knowfreq 55%

basics

~20 s

They fire in that order during one git commit: pre-commit inspects the staged content before any message exists, prepare-commit-msg seeds or rewrites the message file before the editor opens, and commit-msg validates the final message. All three can abort by exiting non-zero.

open as a page

In Git, what are the post-checkout, post-merge and post-commit hooks typically used for?

level: middleimportance: should knowfreq 30%

basics

~20 s

They 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.

open as a page

What does Git's pre-push hook receive on stdin, and what can it check before a push?

level: middleimportance: should knowfreq 40%

basics

~20 s

Git passes the remote's name and URL as arguments and streams one line per ref on stdin: local ref, local object name, remote ref, remote object name. The hook can inspect the exact commit range being pushed and abort the whole push by exiting non-zero.

open as a page

Why can't client-side Git hooks be trusted to enforce a mandatory engineering policy?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Client-side hooks live in .git/hooks, which is not part of the repository content and never travels with a clone, and any developer can skip them with --no-verify or by deleting the file. They are fast feedback, not enforcement.

open as a page