cloudparts

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 ↓
Live model
What you’re watchingCommits pass the local `pre-commit` and `commit-msg` hooks, which send some back to the author. A commit made with `--no-verify` (orange) skips them, so the server’s `pre-receive` hook checks every push again before it reaches `main`.

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.

Think of it as

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-msg for 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 to pre-push or 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.

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.

HookRunsCan it stop the action?Typical use
`pre-commit`Before the commit message is preparedYes; skipped by --no-verifyFormat and lint staged files, scan for secrets
`prepare-commit-msg`After Git writes the default message, before the editor opensYes, but it is meant for editing the message and isn’t skipped by --no-verifyFill in a template; tidy generated messages for merges and squashes
`commit-msg`After the message is written, for git commit and git mergeYes; skipped by --no-verifyCheck or normalize the message, such as Conventional Commits
`post-commit`After the commit is madeNoNotifications
`pre-push`Before git push sends anything; the refs arrive on standard inputYes, the whole pushChecks too slow for every commit
`pre-receive` (server)Once per push, before any ref is updatedYes, the whole pushPolicy for everyone: message format, blocked files, sensitive data
`update` (server)Once per ref being updatedYes, that ref onlyFast-forward-only branches, refuse unannotated tags
`post-receive` (server)After the refs are updatedNoSend commit emails, notify other systems
  • git init copies sample scripts into .git/hooks with a .sample suffix; 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-msg

Often 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 update could 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

  1. 2005

    Git gains its update hook (July 31), sample hooks (August 2), and pre-commit, commit-msg and post-commit (August 18)

  2. 2014

    Husky is first published to npm (June)

  3. 2016

    Git 2.9 adds core.hooksPath (June 13)

  4. 2022

    Git 2.36 ignores repositories owned by other users unless listed in safe.directory, partly to avoid their hooks

  5. 2026

    Git 2.54 lets config files define hooks, several per event (April 20)

Sources13

Improve this page on GitHub