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/*| Hook | Runs | Typical use |
|---|---|---|
pre-commit | Before the commit object is created | Formatting, linting, secret scanning |
prepare-commit-msg | Before the message opens in the editor | Prefix the branch or ticket number |
commit-msg | After you write the message | Enforce a commit-message convention |
post-commit | After the commit exists | Notifications, notes |
pre-push | Before objects are transferred | Run the test suite, block a push to a protected branch |
post-checkout / post-merge | After a checkout or merge | Reinstall dependencies, run migrations |
pre-receive (server) | For every pushed reference | Rules nobody can bypass from their machine |
update (server) | Per reference, before it is accepted | Protect 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-verifywithin 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 command | User-facing equivalent |
|---|---|
git rev-parse HEAD | git log -1 --format=%H |
git ls-files | No porcelain equivalent — listing tracked files needs plumbing |
git diff-index --quiet | git status, but with a usable exit code |
git for-each-ref | git branch, without the interactive formatting |
git status --porcelain | git 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 --quietanswers "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.
Related
Installing Git and configuring your environment Repository health, LFS and recovery
Last refreshed 2026-09-18.