Hooks and Automation

Where each hook fires, why client hooks are feedback rather than enforcement, and how to share them without asking every developer to run a setup script.

advanced 18 min lesson hands-on task included

Hooks are executable files that Git runs at specific moments. A non-zero exit aborts the operation. That is the entire mechanism — and the interesting question is not how to write one, but which layer is actually enforcing your rules.


Topic 1: Where Hooks Fire

HOOKS ARE EXECUTABLE FILES IN .git/hooks — A NON-ZERO EXIT ABORTS THE OPERATION CLIENT SIDE — your machine, easy to skip with --no-verify pre-commit lint, format, secret scan prepare-commit-msg template the message commit-msg enforce the format pre-push run the fast tests SERVER SIDE — the forge, cannot be skipped pre-receive reject the whole push update reject one ref post-receive notify, deploy, trigger CI HOOKS ARE NOT A CONTROL .git/hooks is never cloned, and --no-verify skips every client hook. Client hooks are fast feedback. CI is the gate. SHARING THEM PROPERLY git config core.hooksPath .githooks …or the pre-commit framework, versioned in-repo and run the identical checks in CI so nothing depends on trust.
Client hooks are on the top row and skippable; server hooks are on the bottom row and are not. The two panels underneath are the design rule: client hooks are fast feedback, CI and server hooks are the gate.

Client-side, in .git/hooks/:

HookFiresTypical use
pre-commitBefore the commit is createdLint, format, secret scan, reject large files
prepare-commit-msgBefore the editor opensInsert a template or a ticket ID from the branch name
commit-msgAfter the message is writtenEnforce a format
post-commitAfter the commitNotifications; cannot abort anything
pre-rebaseBefore a rebaseRefuse to rebase a protected branch
pre-pushBefore objects are sentRun the fast test suite
post-checkout / post-mergeAfter the working tree changesReinstall dependencies when the lockfile changed

Server-side, on the remote:

HookFiresTypical use
pre-receiveOnce, before any ref is updatedReject the whole push — policy, secret scan, signature checks
updateOnce per refReject one branch’s update
post-receiveAfter all refs are updatedTrigger CI, notify chat, deploy

Server hooks cannot be skipped by the person pushing, which is what makes them a control rather than a suggestion. On a hosted forge you do not get to write them directly — branch protection rules, required checks, and push protection are the managed equivalents.


Topic 2: Client Hooks Are Not Enforcement

Two facts that decide how you should use them:

  1. .git/hooks is never cloned. Hooks are not part of the repository’s content, so a fresh clone has none of yours.
  2. --no-verify skips every client hook. git commit --no-verify, git push --no-verify. There is no configuration that prevents this, because the hook runs on the developer’s own machine and they own it.

So a client hook is fast feedback, and that is genuinely valuable: catching a lint error in two seconds beats catching it in a six-minute CI run. It is not a control. Any rule that actually matters must also exist in CI or in a server-side check, running the identical command.

The pattern that follows: write the check once as a script, call it from both the hook and the pipeline.


Topic 3: Sharing Hooks

mkdir .githooks
git config core.hooksPath .githooks

core.hooksPath points Git at a directory you can commit. Every developer still has to run the git config line once — put it in your make setup or bootstrap script, or in a post-checkout hook that only needs to run once.

A usable pre-commit:

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

# Reject large files before they enter history forever
for f in $(git diff --cached --name-only --diff-filter=ACM); do
    [ -f "$f" ] || continue
    size=$(wc -c < "$f")
    if [ "$size" -gt 1048576 ]; then
        echo "error: $f is $((size / 1024))KB. Use Git LFS or add it to .gitignore." >&2
        exit 1
    fi
done

# Format only what is staged
staged_go=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$')
if [ -n "$staged_go" ]; then
    gofmt -l $staged_go | grep . && {
        echo "error: run gofmt on the files above" >&2
        exit 1
    }
fi

And a commit-msg:

#!/bin/sh
# .githooks/commit-msg
subject=$(head -1 "$1")

# Ignore merge and fixup commits — Git generates those
case "$subject" in
    Merge*|Revert*|fixup!*|squash!*) exit 0 ;;
esac

if [ ${#subject} -gt 72 ]; then
    echo "error: subject is ${#subject} characters; keep it under 72." >&2
    exit 1
fi

if ! echo "$subject" | grep -qE '^(feat|fix|docs|chore|refactor|test|perf)(\(.+\))?!?: .+'; then
    echo "error: use conventional commits, e.g. 'fix(auth): reject expired tokens'" >&2
    exit 1
fi

Both must be executable (chmod +x), and both should exit fast — a hook that adds four seconds to every commit gets bypassed within a week, which is worse than not having it.

The pre-commit framework manages this properly for teams: hooks are declared in a committed YAML file, installed with one command, versioned, and cached. It is worth adopting the moment more than one hook exists.


Topic 4: What to Automate, and What Not To

Worth a hook:

  • Secret scanning — catching a credential before it is committed is enormously cheaper than after.
  • Rejecting large binaries — the alternative is a history rewrite.
  • Formatting on staged files only.
  • Commit message format, if you generate changelogs from it.
  • A pre-push that runs the fast tests, when the suite is genuinely fast.

Not worth a hook:

  • The full test suite on pre-commit. People commit constantly; a slow hook trains everyone to use --no-verify, which disables your secret scan too.
  • Anything requiring network access, which turns an offline commit into a failure.
  • Anything with a false-positive rate. A hook that blocks legitimate work loses its authority immediately.

The rule: if it takes more than a couple of seconds, it belongs in pre-push or in CI, not in pre-commit.


Topic 5: Hooks Versus the Forge

On GitHub, GitLab or Bitbucket you cannot install a pre-receive hook. The managed equivalents do the same jobs:

GoalSelf-hosted hookHosted forge feature
Reject force-push to mainpre-receiveBranch protection
Require reviewpre-receiveRequired approvals + CODEOWNERS
Require green CIpre-receiveRequired status checks
Block secretspre-receive scanPush protection / secret scanning
Require signed commitspre-receive verify”Require signed commits” setting
Enforce commit formatcommit-msg + CIA CI job (the reliable layer)
Trigger a buildpost-receiveWebhooks / Actions / Pipelines

Configure these once per repository, or once per organisation with a template — and prefer the organisation-level setting, because a per-repository control is only as good as whoever created the last repository.


Topic 6: Automation Beyond Hooks

Two Git features that behave like automation and are not hooks:

.gitattributes merge drivers — decide how specific files merge, which removes recurring manual conflicts:

CHANGELOG.md merge=union          # keep both sides' lines
*.generated.go -diff -merge       # never diff or merge; regenerate instead

git config alias with shell escapes — small, sharp, repo-agnostic commands:

git config --global alias.cleanup \
  '!git branch --merged main | grep -v "^\*\|main" | xargs -r git branch -d'
git config --global alias.recent \
  'branch --sort=-committerdate --format="%(committerdate:short) %(refname:short)"'

And the maintenance job that keeps a large repository responsive without anyone thinking about it:

git maintenance start     # schedules gc, commit-graph, prefetch in the background

That single command replaces the folklore of running git gc manually, and on a large repository it measurably improves log and status times by keeping the commit-graph current.

Try it yourself: write a pre-commit that rejects a staged file over 1 MB, confirm it blocks the commit, then commit the same file with --no-verify. Watching your own control get bypassed in one flag is the fastest way to internalise where enforcement actually has to live.

Common mistake: treating client-side hooks as a security or compliance control and reporting them as such. They run on machines you do not control, they are skippable with a documented flag, and a fresh clone has none of them. They are excellent developer experience and they are not evidence of anything — the CI job running the same check is what you can actually point an auditor at.