Git hooks
Programs Git runs at fixed points, such as just before a commit is made or before a push is accepted, so checks and automation happen without anyone remembering to run them.
1 technology on this page ↓How it works
A hook is any executable file named after an event, such as pre-commit or pre-push, placed in the repository’s hooks directory: .git/hooks by default, or wherever core.hooksPath points. Git runs it at that moment and passes details as arguments or on standard input; when a hook that runs before an action, such as pre-commit, commit-msg or pre-push, exits with a non-zero status, Git aborts the action. Client-side hooks run on the developer’s machine. pre-commit runs before the commit message is prepared and is where formatters, linters and quick tests go; commit-msg gets the path of the message file and may rewrite or reject it; pre-push reads the refs about to be sent. git commit --no-verify skips pre-commit and commit-msg. Server-side hooks run in the repository that receives a push: pre-receive reads every ref update on standard input and can reject the whole push, update runs once per ref and can reject just that ref, and post-receive runs after the refs have moved, for notifications. Hooks are not tracked files and git clone doesn’t copy them, so teams share them another way: a committed directory plus core.hooksPath; a hook manager such as Husky, Lefthook or pre-commit that reads a checked-in config and installs itself into Git; or, since Git 2.54, named hooks in Git’s own config (hook.<name>.command and hook.<name>.event), which let several commands run for one event and can be set once for every repository. Anyone can skip or delete a local hook, so a rule that must hold is checked again in CI or by a server-side hook. Custom server hooks need a server you administer, such as GitHub Enterprise Server or GitLab Self-Managed; GitLab offers push rules as “a user-configurable Git hook interface” for the same jobs.
A checklist taped inside your own front door versus the security check at the airport. The checklist (a client-side hook) catches the forgotten passport before you leave, but you can walk past it; the airport check (a server-side hook) stops everyone, whatever they remembered.
Reach for it when
- Fast checks on what is about to be committed: formatting, linting and secret scanning of the staged files
- Checking or fixing commit messages before they exist, such as commitlint in
commit-msgfor Conventional Commits - Rules a self-hosted server must enforce for every pusher, such as fast-forward-only branches or no unannotated tags (
pre-receive,update)
Think twice when
- Slow work such as the full test suite in
pre-commit: people start reaching for--no-verify; move it topre-pushor CI - As the only line of defense: local hooks aren’t cloned and can be skipped, so CI or the server must check again
- Running hooks from a repository you don’t trust: a hook is an arbitrary program that runs with your permissions
A real example
A 12-person TypeScript team uses Husky. Its pre-commit hook runs lint-staged, which formats and lints only the 3 staged files instead of all 2,000 in the repository, so the commit waits about two seconds; its commit-msg hook runs commitlint. One developer commits a work-in-progress with --no-verify and pushes it. The same lint job runs in CI on the pull request, fails, and the branch can’t merge until it’s fixed, so nothing unformatted reaches main.
Technologies
Specific products, and what each does differently.
| Open source | pre-commit dev team (Anthony Sottile, Ken Struys) | MIT | 2026-10-13 |
The hooks teams use most#
Git supports more than twenty hooks; these are the ones most setups touch. Client-side hooks run in your clone, server-side hooks in the repository that receives the push.
| Hook | Runs | Can it stop the action? | Typical use |
|---|---|---|---|
| `pre-commit` | Before the commit message is prepared | Yes; skipped by --no-verify | Format and lint staged files, scan for secrets |
| `prepare-commit-msg` | After Git writes the default message, before the editor opens | Yes, but it is meant for editing the message and isn’t skipped by --no-verify | Fill in a template; tidy generated messages for merges and squashes |
| `commit-msg` | After the message is written, for git commit and git merge | Yes; skipped by --no-verify | Check or normalize the message, such as Conventional Commits |
| `post-commit` | After the commit is made | No | Notifications |
| `pre-push` | Before git push sends anything; the refs arrive on standard input | Yes, the whole push | Checks too slow for every commit |
| `pre-receive` (server) | Once per push, before any ref is updated | Yes, the whole push | Policy for everyone: message format, blocked files, sensitive data |
| `update` (server) | Once per ref being updated | Yes, that ref only | Fast-forward-only branches, refuse unannotated tags |
| `post-receive` (server) | After the refs are updated | No | Send commit emails, notify other systems |
git initcopies sample scripts into.git/hookswith a.samplesuffix; remove the suffix and make the file executable to enable one.
In code · bash
# A hook is an executable named after the event
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
# a non-zero exit aborts the commit
git diff --cached --check || exit 1 # whitespace errors in staged changes
exec npm run lint
EOF
chmod +x .git/hooks/pre-commit
# Share hooks by committing them and pointing Git at the folder
git config core.hooksPath .githooks
# Git 2.54+: named hooks in config, several per event
git config set hook.lint.command "npm run lint"
git config set --append hook.lint.event pre-commit
git hook list pre-commit
git commit --no-verify -m "WIP" # skips pre-commit and commit-msgOften confused with
Where it came from2005–2026
Hooks arrived in Git’s first summer. On July 31, 2005, less than four months after Git’s first commit, a patch credited to Josef Weidendorfer made git-receive-pack run $GIT_DIR/hooks/update with the ref name and the old and new commit IDs just before updating a ref. Junio C Hamano rewrote it so the hook could decline the update by exiting with a non-zero status, and days later split out a separate post-update hook, partly on a suggestion from Linus Torvalds that refreshing the files dumb HTTP clients need should be optional. On August 2 Hamano added a template directory that git init copies into every new repository, to ship sample hooks. On August 18 came pre-commit, commit-msg and post-commit, and Git’s built-in commit checker moved into the sample pre-commit hook so each project could customize it.
Because the hooks directory lives inside .git, hooks were never versioned with the code, and Git’s own book warns that client-side hooks are not copied when you clone. Tools filled the gap. Husky, first published to npm in June 2014, installed hooks for JavaScript projects. The pre-commit framework was built because its authors “copied and pasted unwieldy bash scripts from project to project”; it became a package manager that installs hooks written in any language. In June 2016 Git 2.9 added core.hooksPath, because hooks stored in each repository’s .git directory were “a pain to manage” for anyone with a standard set; current Husky relies on it. Later managers such as Evil Martians’ Lefthook, a single Go binary, added parallel runs and per-file filtering.
Git itself caught up in 2026. Git 2.54, released on April 20, lets any config file, global or system-wide included, define named hooks with hook.<name>.command and attach them to events with hook.<name>.event, so several commands can run for one event and one script can serve many repositories; Git 2.55 in June let them run in parallel. Hooks also became a security concern: since Git 2.36 Git refuses to work in a repository owned by another user unless it is listed in safe.directory, to avoid being affected by that user’s config files and hooks.
Why it’s built this way
- Hooks are plain executables named after the event
- Git needs no plugin API: it passes arguments and standard input and reads the exit status. The samples are shell and Perl, but Pro Git notes that any properly named executable works, in whatever language you know.
- A non-zero exit can refuse the action
- Weidendorfer’s first version ran after the update. Hamano changed it so
updatecould decline a ref before it moved, which is what makes hooks usable for policy rather than only for notifications. - Hooks stay out of clones
- A hook is a program that runs with your permissions, so running ones that arrive with a clone would hand control to whoever published the repository. The cost is that teams need another way to share hooks, which is why core.hooksPath and the hook managers exist.
- Named hooks in config (Git 2.54)
- One file per event per repository made it awkward to run a linter and a leak detector together, or to reuse one script across many repositories. Named hooks can be set globally, several can attach to one event, and wrapper tools can reuse the mechanism through
git hook run.
Timeline
- 2005
Git gains its
updatehook (July 31), sample hooks (August 2), andpre-commit,commit-msgandpost-commit(August 18) - 2014
Husky is first published to npm (June)
- 2016
Git 2.9 adds
core.hooksPath(June 13) - 2022
Git 2.36 ignores repositories owned by other users unless listed in
safe.directory, partly to avoid their hooks - 2026
Git 2.54 lets config files define hooks, several per event (April 20)
- git/git on GitHub: Initial revision of “git” (Apr 7, 2005)
- git/git on GitHub: [PATCH] Added hook in git-receive-pack, Josef Weidendorfer (Jul 31, 2005)
- git/git on GitHub: receive-pack hooks updates (Aug 2, 2005)
- git/git on GitHub: Install sample hooks (Aug 2, 2005)
- git/git on GitHub: Add commit hook and make the verification customizable (Aug 18, 2005)
- Pro Git: Customizing Git, Git Hooks (client-side hooks are not copied when you clone)
- npm registry: husky package metadata (first published June 9, 2014)
- pre-commit: Introduction (why the framework was built)
- The GitHub Blog: Git 2.9 has been released (Jun 13, 2016)
- Git 2.9 release notes: core.hooksPath
- Lefthook README: single Go binary, parallel execution, glob filters
- Git 2.36 release notes: repositories owned by other users and their hooks
- Git 2.54 release notes: hook commands defined in configuration files
- Git 2.55 release notes: configured hooks can run in parallel
- Git docs: githooks (manual change history with release dates)
Sources13
- Git docs: githooks (every hook, its arguments, exit status and --no-verify)
- Git docs: git-hook (hook.<name>.command, hook.<name>.event, git hook run and list)
- Pro Git: Customizing Git, Git Hooks
- Git 2.9 release notes: core.hooksPath
- Git 2.54 release notes: hooks defined in configuration
- Husky docs: features (uses core.hooksPath, supports all 13 client-side hooks)
- Husky docs: Get started (husky init, disabling with HUSKY=0)
- Lefthook README: Git hooks manager, single Go binary, parallel execution
- pre-commit: quick start (.pre-commit-config.yaml, pre-commit install)
- lint-staged README: run tasks against staged files only
- commitlint docs: Local setup (lint messages in the commit-msg hook)
- GitHub Docs: About pre-receive hooks (GitHub Enterprise Server)
- GitLab Docs: Git server hooks (GitLab Self-Managed; push rules as a user-configurable alternative)