Hooks, aliases and automation

Client and server hooks, pre-commit and commit-msg in practice, formatters and linters that run on every commit, and scripting Git with plumbing commands.

Where hooks run

# client hooks live in .git/hooks and are not cloned
ls .git/hooks

# so ship them in a tracked directory instead
git config core.hooksPath .githooks
chmod +x .githooks/*
HookRunsTypical use
pre-commitBefore the commit object is createdFormatting, linting, secret scanning
prepare-commit-msgBefore the message opens in the editorPrefix the branch or ticket number
commit-msgAfter you write the messageEnforce a commit-message convention
post-commitAfter the commit existsNotifications, notes
pre-pushBefore objects are transferredRun the test suite, block a push to a protected branch
post-checkout / post-mergeAfter a checkout or mergeReinstall dependencies, run migrations
pre-receive (server)For every pushed referenceRules nobody can bypass from their machine
update (server)Per reference, before it is acceptedProtect a branch, require signed commits

A hook is an ordinary executable. Git runs it, and a non-zero exit status stops the operation — that is the entire mechanism, and it is why hooks are the cheapest automation in a repository.

A pre-commit and commit-msg hook

#!/bin/sh
# .githooks/pre-commit
set -e

files=$(git diff --cached --name-only --diff-filter=ACM -- "*.js" "*.ts")
[ -z "$files" ] && exit 0

echo "$files" | xargs npx prettier --write
echo "$files" | xargs npx eslint --max-warnings=0

# a formatter rewrote the files, so stage them again
git add $files
#!/bin/sh
# .githooks/commit-msg
msg=$(head -1 "$1")
echo "$msg" | grep -Eq '^(feat|fix|docs|chore|refactor|test): .+' || {
  echo "Commit subject must look like 'fix: handle request timeouts'"
  exit 1
}
  • Check only the staged files. Running a linter over the whole repository on every commit is how a hook becomes something people disable.
  • Re-add files after a formatter rewrites them, or the commit contains the unformatted content while the working tree looks clean.
  • Keep hooks fast: anything over a few seconds gets bypassed with --no-verify within a week.
  • Frameworks such as husky, pre-commit and lefthook mostly manage installation and language runtimes; underneath they are still a script and an exit code.
⚠️
A pre-commit hook runs on the developer's machine with the developer's permissions, and --no-verify skips it entirely. Anything that genuinely protects the repository — secrets, licence checks, protected branches — must also be enforced in CI or on the server, never only locally.

Aliases and plumbing

# aliases, usually in ~/.gitconfig
[alias]
  st = status -sb
  lg = log --oneline --graph --decorate --all
  last = log -1 --stat
  unstage = restore --staged
  amend = commit --amend --no-edit
  wip = !git add -A && git commit -m "wip"

# plumbing: stable output that scripts can rely on
git rev-parse --abbrev-ref HEAD
git rev-parse HEAD
git ls-files --modified
git diff-index --quiet HEAD -- || echo "working tree is dirty"
git for-each-ref --format="%(refname:short) %(objectname:short)" refs/heads
Plumbing commandUser-facing equivalent
git rev-parse HEADgit log -1 --format=%H
git ls-filesNo porcelain equivalent — listing tracked files needs plumbing
git diff-index --quietgit status, but with a usable exit code
git for-each-refgit branch, without the interactive formatting
git status --porcelaingit status in a parse-friendly form
  • Plumbing output is designed to be stable across versions; porcelain output is designed for humans and may change.
  • Scripts should check exit codes rather than parse prose — git diff-index --quiet answers "is it dirty" with a status, not a string.
  • An alias starting with ! runs a shell command, which is powerful and easy to abuse; keep them short and predictable.

FAQ

Why did my hook stop running?
Usually one of three things: the file is not executable (chmod +x), the shebang line is missing or wrong, or the hook was never cloned because .git/hooks is not part of the repository. Point core.hooksPath at a tracked directory.
Should I use a framework or plain scripts?
Plain scripts are transparent and have no dependencies. A framework earns its place when you need per-language runners, parallel checks and one-command installation for a large team.

Installing Git and configuring your environment Repository health, LFS and recovery

Last refreshed 2026-09-18.