Free lesson · Git Mastery
Hooks Overview
Hooks Overview
A note from Swati Bajpai, Engineering Manager at Google Search Infrastructure.
Automation at Lifecycle Points
Git hooks are scripts that run automatically at specific points in Git's lifecycle. When you commit, a sequence of hooks can fire. When you push, another sequence. When the server receives a push, server-side hooks fire. The hook system is what lets teams enforce conventions, run checks, and integrate Git operations with broader workflows without engineers having to remember to run anything manually.
The mechanism is simple. Git looks in .git/hooks/ for files with specific names; if found, executable, and matching one of Git's recognized hook events, Git runs them at the right moment. The hooks can succeed (letting the operation proceed) or fail (blocking the operation). What the hook script does between those endpoints is entirely up to the team.
I manage a fourteen-person team at Google Search Infrastructure. Hooks are part of how we maintain code quality and operational discipline across the team. Without them, every check would require manual discipline; with them, the checks happen automatically. This topic frames what hooks are and the categories. The rest of Module 17 covers specific hooks, frameworks, and patterns.
The Hook Lifecycle
Git's operations have hook points at various moments. The most commonly used:
Client-side hooks (run on the developer's machine):
pre-commit- before a commit is createdprepare-commit-msg- before the commit message editor openscommit-msg- after the commit message is finalized, before the commit is createdpost-commit- after the commit is createdpre-push- before a push to a remotepost-checkout- after switching branches or files
Server-side hooks (run on the remote when receiving pushes):
pre-receive- before any refs are updatedupdate- for each ref being updatedpost-receive- after all refs are updated
The client-side hooks let developers catch issues before they reach the remote. Server-side hooks let the remote enforce policies regardless of what the developer's machine did. The two layers work together - client-side hooks give fast feedback; server-side hooks provide the actual enforcement.
There are more hook events; the list above covers the ones teams use most. The full set is documented in the Git man page for githooks.
Where Hooks Live
By default, hooks live in .git/hooks/ in each repository:
ls -la .git/hooks/
A fresh repository has sample 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
pre-rebase.sample
pre-receive.sample
prepare-commit-msg.sample
push-to-checkout.sample
update.sample
The .sample files are inactive - Git only runs hooks without the .sample extension. To activate a hook, you create or rename a file with the proper hook name (no .sample), make it executable, and Git will run it at the right time. The samples show the pattern; they're starting points, not active hooks.
The hooks directory is local to each clone. It isn't shared via push or pull. This is important: hooks you've configured don't automatically apply to other team members' clones.
A Hook's Lifecycle in Detail
A hook is just a script. The hook event determines:
- The hook's filename (
pre-commit,pre-push, etc.) - What arguments and stdin Git provides
- What exit code semantics mean (0 = continue; non-zero = abort)
- When in the operation it runs
For example, pre-commit runs before a commit is created. It receives no arguments. If it exits 0, the commit proceeds; if it exits non-zero, the commit is aborted with the script's stderr shown to the user.
# A minimal pre-commit hook
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
if grep -r "TODO_FIXME" --include="*.py" .; then
echo "ERROR: Found TODO_FIXME markers in Python files"
exit 1
fi
exit 0
The hook is a bash script. It checks for TODO_FIXME markers in Python files. If any are found, it prints an error and exits non-zero, blocking the commit. If none are found, it exits zero and the commit proceeds. The shebang line at the top tells the system this is a bash script; the file needs to be executable for Git to run it.
The discipline: hooks should be fast (they run on every operation), should give clear error messages, and should be skippable in emergencies (Git supports --no-verify to bypass most client-side hooks).
What Hooks Are Used For
The patterns I see across teams:
Style enforcement. Pre-commit hooks run linters and formatters. Code with style issues either auto-formats or fails to commit.
Test running. Pre-commit hooks run fast unit tests. Slow tests defer to CI.
Security checks. Pre-commit and pre-push hooks scan for secrets, credentials, or other sensitive data.
Convention enforcement. Commit-msg hooks verify commit message format. Pre-push hooks verify branch naming.
Workflow automation. Post-commit hooks update local state. Post-checkout hooks switch environment configuration.
Server-side policy. Pre-receive hooks reject pushes that violate team policies - bad commit messages, force-pushes to protected branches, missing approvals.
Each pattern has tradeoffs. Client-side hooks are fast but bypassable. Server-side hooks are authoritative but feedback is later. Most teams use a combination.
A Visualization
flowchart LR
A[Developer edits code] --> B[git add]
B --> C[git commit]
C --> PC{pre-commit hook}
PC -->|pass| PM{prepare-commit-msg}
PC -->|fail| X[Commit aborted]
PM --> CM{commit-msg hook}
CM -->|pass| Commit[Commit created]
CM -->|fail| X
Commit --> Post{post-commit hook}
Post --> Done[Commit complete]
Done --> Push[git push]
Push --> PP{pre-push hook}
PP -->|pass| Remote[Push to remote]
PP -->|fail| X2[Push aborted]
Remote --> PR{server: pre-receive}
PR -->|pass| Accept[Refs updated]
PR -->|fail| X3[Push rejected]
Accept --> PostR{server: post-receive}
PostR --> Final[Push complete]
style PC fill:#dcfce7
style PM fill:#dcfce7
style CM fill:#dcfce7
style Post fill:#dcfce7
style PP fill:#dcfce7
style PR fill:#dbeafe
style PostR fill:#dbeafe
The diagram traces a commit-and-push flow with all the hooks that may fire. Green boxes are client-side hooks running on the developer's machine. Blue boxes are server-side hooks running on the remote. Each hook can either allow the operation to continue or abort it. The chain is what gives Git's hook system its power - many opportunities to inject team-specific behavior.
A Pattern at Google Search Infrastructure
The team uses hooks across several layers:
Client-side (shared via the team's tooling repo):
pre-commit- runs linters, formatters, fast testscommit-msg- verifies our commit message formatpre-push- runs medium-speed integration tests on the changed code
Server-side (configured at the Git hosting level):
pre-receive- verifies branch protection rulespost-receive- notifies CI to start builds
The two layers work together. Client-side hooks give fast feedback - developers know within seconds whether their commit will pass the basic checks. Server-side hooks enforce the rules absolutely - even if a developer bypassed the client-side hooks with dash dash no dash verify, the server side still applies.
The team's hooks are documented and shared. Topic 17.7 covers how teams distribute hooks; Topic 17.8 covers custom patterns.
A Specific Story From My Stripe Days
When I was a Principal Engineer at Stripe around 2018, I worked with a team that had no client-side hooks. Every check happened in CI. The CI runs took 15 to 25 minutes; developers would push, wait, find the CI failed for a simple issue, fix it locally, push again, and wait another 15-25 minutes.
The cycle was costing us substantial productivity. The same checks that took 30 seconds locally were taking 30 minutes in CI because of the queueing and setup overhead.
We deployed a basic set of client-side hooks: linter check, format check, simple test run. The hooks took about 20 seconds total. Developers got feedback before they pushed. The CI was still authoritative - but it now failed rarely because the obvious issues were caught locally.
The result: about a 40% reduction in CI-related wait time across the team. The hooks paid back their setup cost within the first week.
The lesson: client-side hooks are a productivity investment. The 20 seconds of local check time is much cheaper than 30 minutes of CI feedback loop. For teams where CI feedback is slow, hooks are one of the highest-leverage changes you can make.
A Common Misconception
A pattern I've seen in inexperienced teams: treating hooks as the enforcement layer. They install hooks, the hooks block bad commits locally, the team thinks they're protected.
But client-side hooks are bypassable. Any developer can use git commit --no-verify to skip them. Any developer can disable their local hooks. The hooks are advisory locally; the actual enforcement has to be server-side or in CI.
The right mental model: client-side hooks are for developer convenience (fast feedback, automatic correction). Server-side hooks are for enforcement (the policies the team requires). The two have different roles and shouldn't be confused.
I see teams skip the server-side layer and rely entirely on client-side hooks. The result is policy drift - the things hooks were supposed to enforce gradually get bypassed, deliberately or accidentally, until the convention erodes.
What Comes Next
Topic 17.2 covers the pre-push hook in depth. Topic 17.3 covers pre-commit. Topic 17.4 covers commit-msg. Topic 17.5 covers handling secrets via hooks. Topic 17.6 covers server-side hooks. Topic 17.7 covers hook frameworks (pre-commit framework, Husky, lefthook). Topic 17.8 covers custom hook patterns and distribution.
What Goes Wrong
Warning: Common hook misconceptions:
Treating client-side hooks as enforcement. They're bypassable; server-side is the real layer.
Making hooks too slow. Hooks that take a minute become hated by developers who use
--no-verifyto bypass them.Forgetting hooks aren't versioned with the repo. Each clone needs hooks set up separately unless using a framework.
Hooks with unclear error messages. Developers can't tell what failed.
No bypass mechanism. Sometimes you genuinely need to skip;
--no-verifyshould work.Different hooks across team members. Inconsistent behavior; convention drift.
Try It
Exercise: Explore hooks:
- In any repository, list
.git/hooks/. Note the sample files.- Open one of the sample hooks. Read it to understand the structure.
- Create a simple
pre-commithook that prints a message. Make it executable.- Make a commit. Verify the hook runs.
- Try
git commit --no-verify. Note the hook is bypassed.- Look at the full hook list in
git help githooks. Note which events you might use.