Skip to content
DevOps devops git 6 min read

Git Hooks

Git hooks are small scripts that Git runs automatically when certain events happen in your repository — for example, right before you create a commit or right before you push to a remote. They let you “shift left”, which means catching problems early (on your own machine, before code is shared) instead of late (after a teammate or a CI server complains). A classic use is running a linter (a tool that checks your code for style and errors) before every commit, so broken code never even gets saved. This page shows where hooks live, how to write one, and the modern managers that make them easy to share with a team.

What a Git hook is

A hook is just an executable file with a specific name placed in a special folder. When the matching event fires, Git runs that file. If the script finishes with an exit code of 0 (success), Git continues. If it exits with any non-zero code (failure), Git stops the action — so a failing pre-commit hook cancels your commit.

There are two broad families:

  • Client-side hooks — run on your own computer (commit, push, merge events). These are what most teams use day to day.
  • Server-side hooks — run on the remote Git server when it receives a push (e.g. to reject bad pushes centrally). Hosted platforms like GitHub usually replace these with their own checks, so beginners can ignore them.

Hooks are not copied when someone clones your repository. The .git/hooks folder is local and never tracked by Git. This is exactly why managers like pre-commit and Husky exist — to share hooks with the whole team.

Where hooks live

Every Git repo has a .git/hooks directory. On Ubuntu, after you git init or clone a project, look inside it:

ls /home/dev/myapp/.git/hooks

Output:

applypatch-msg.sample      pre-merge-commit.sample
commit-msg.sample          pre-push.sample
post-update.sample         pre-rebase.sample
pre-applypatch.sample      prepare-commit-msg.sample
pre-commit.sample          update.sample

Those .sample files are disabled examples. Git only runs a hook whose name has no .sample extension and is executable. So to enable a hook you create (or rename) a file like pre-commit and mark it runnable with chmod +x.

Common client-side hooks

HookWhen it runsTypical use
pre-commitBefore the commit is createdLint, format, run fast unit tests
prepare-commit-msgBefore the commit message editor opensInsert a template or ticket number
commit-msgAfter you write the messageEnforce a message format (e.g. Conventional Commits)
pre-pushBefore objects are sent to a remoteRun the full test suite, block pushing to main
post-mergeAfter a successful git merge / pullRe-install dependencies if package.json changed

When to use hooks (and when not to)

Use hooks for fast, local feedback: formatting, linting, secret-scanning, and quick checks that take a few seconds. The goal is to fail in one second on your laptop instead of ten minutes later in CI.

Do not rely on hooks as your only safety net. A developer can skip any client-side hook with git commit --no-verify. Hooks improve the day-to-day experience, but the real gate must be your CI pipeline (the automated checks that run on the server for every push or pull request). Treat hooks as a convenience, not a security control.

Writing a pre-commit hook by hand

Let’s create a hook that runs a linter before each commit. Imagine a Node.js project that uses ESLint (a popular JavaScript linter). Create the file:

cd /home/dev/myapp
nano .git/hooks/pre-commit

Put this inside:

#!/usr/bin/env bash
# Stop the commit if linting fails.
set -e

echo "Running ESLint on staged files..."

# Only lint JS/TS files that are staged for this commit.
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts)$' || true)

if [ -z "$files" ]; then
  echo "No JS/TS files staged — skipping lint."
  exit 0
fi

npx eslint $files

Now make it executable (Git ignores non-executable hooks):

chmod +x .git/hooks/pre-commit

Try a commit with a lint error in a staged file:

git add app.js
git commit -m "add feature"

Output:

Running ESLint on staged files...

/home/dev/myapp/app.js
  3:7  error  'total' is assigned a value but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

Because ESLint exited with a non-zero code and set -e is on, the script stops, the hook fails, and the commit is rejected. Fix the issue, re-stage, and commit again — this time it passes and the commit goes through.

Using a hook manager: pre-commit

Writing hooks by hand is fine for one repo, but they live in .git/hooks and can’t be shared. The pre-commit framework solves this with a single tracked config file. Install it on Ubuntu:

sudo apt update
sudo apt install -y pipx
pipx install pre-commit

Add a .pre-commit-config.yaml to your project root (this file is committed, so everyone gets the same checks):

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v9.10.0
    hooks:
      - id: eslint

Then install it into the repo’s .git/hooks once:

pre-commit install

Output:

pre-commit installed at .git/hooks/pre-commit

Now every git commit runs all the checks automatically. New teammates just run pre-commit install after cloning.

Using a hook manager: Husky

For JavaScript and TypeScript teams, Husky is the most popular manager because it lives inside package.json and npm. Set it up:

cd /home/dev/myapp
npm install --save-dev husky
npx husky init

husky init creates a .husky/ folder (which is committed) and adds a prepare script so hooks install automatically on npm install. Edit the generated .husky/pre-commit:

npx lint-staged

Pair it with lint-staged, which runs tools only on the files you actually staged (much faster). Add this to package.json:

"lint-staged": {
  "*.{js,ts}": "eslint --fix"
}

Now commits are linted and auto-fixed across the whole team — no manual .git/hooks editing required.

pre-commit vs Husky — which to use

pre-commitHusky
Language ecosystemAny (Python, Go, JS, mixed)JavaScript / TypeScript focused
Config file.pre-commit-config.yaml.husky/ scripts + package.json
Install dependencypipx / Pythonnpm / Node.js
Best forPolyglot repos, Python projectsNode projects already using npm

Best Practices

  • Keep hooks fast. A pre-commit hook over a couple of seconds annoys developers and tempts them to bypass it. Save slow tests for pre-push or CI.
  • Only check staged files, not the whole project — use git diff --cached or lint-staged so commits stay snappy.
  • Use a manager (pre-commit or Husky), not hand-written .git/hooks, so the whole team gets the same checks automatically.
  • Pin versions (rev: in pre-commit, exact versions in package.json) so hooks behave identically on every machine.
  • Mirror your hooks in CI. Since --no-verify can skip any local hook, the same lint/test gate must also run on the server.
  • Make hooks exit non-zero on failure so Git actually blocks the action — a hook that always exits 0 does nothing.
Last updated June 15, 2026
Was this helpful?