Git hooks are scripts that run automatically at fixed points in the workflow — before a commit is created, before a push leaves the machine, after a checkout. They turn "remember to run the linter" into something that runs because the workflow made it run.

Where they live and what exists #

The hooks directory, and the menu
$ ls .git/hooks/            # samples end in .sample; rename to activate
applypatch-msg  pre-rebase    pre-push      post-checkout
prepare-commit-msg  pre-commit  commit-msg  post-merge

A hook is any executable script named exactly like the event — no extension, marked executable, in .git/hooks/. Hooks are not versioned with the repository (they are in .git/), which is the central logistics problem of sharing them — solved below.

The two hooks everyone starts with #

pre-commit runs before the commit is created; exiting non-zero cancels it:

.git/hooks/pre-commit — refuse broken commits
#!/usr/bin/env bash
set -e
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$' || true)
[ -z "$files" ] && exit 0
echo "$files" | xargs npx eslint --max-warnings 0

commit-msg receives the message file as $1 — the format police:

.git/hooks/commit-msg — enforce the message style
#!/usr/bin/env bash
head -1 "$1" | grep -qE '^(feat|fix|chore|docs|refactor|test)(\(.+\))?: .+' \
  || { echo "commit message must be 'type: summary'" >&2; exit 1; }

Sharing hooks: the logistics #

Because .git/hooks/ is not tracked, teams do one of:

ApproachHow
Committed hook files + install stephooks/ in repo; git config core.hooksPath hooks
husky / pre-commit frameworksTooling manages install on setup
Symlinks from a setup scriptSetup creates the links once
The one-liner that makes hooks versionable
$ git config core.hooksPath .githooks
$ # .githooks/ is now a tracked directory of hook scripts

core.hooksPath is the clean answer: commit the hooks like any script, point git at the directory, and every clone gets them after one config line — which the project README or a setup script runs.

What hooks can and cannot do #

Hooks are local: a pre-push hook on your machine does not run on a teammate's, and no hook can stop a determined --no-verify. So the architecture is: hooks for fast, local gates (format, lint, message style), CI for authoritative gates (build and test on the server). The two layers agree on the same commands, so surprises stay rare.

The commits hooks guard are add and commit; the messages they police follow the message format. The Git series continues.