Client-Side Hooks
Client-side hooks run on your local machine in response to Git operations you perform. They are the developer's first line of defence — catching lint errors, enforcing commit message formats, and running quick tests before code ever leaves your workstation.
pre-commit
pre-commit fires before Git even opens the commit message editor. It receives no arguments. If it exits non-zero, the commit is aborted. Use it to run formatters, linters, and fast unit tests against the staged files.
.git/hooks/pre-commit — lint only staged JS/TS files
#!/usr/bin/env bash set -euo pipefail # Get the list of staged files STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep -E '.(js|ts|jsx|tsx)$' || true) if [ -z "$STAGED" ]; then exit 0 fi echo "Running ESLint on staged files..." echo "$STAGED" | xargs ./node_modules/.bin/eslint --max-warnings=0 echo "Running Prettier check on staged files..." echo "$STAGED" | xargs ./node_modules/.bin/prettier --check echo "✓ pre-commit checks passed"
.git/hooks/pre-commit — run fast tests
#!/usr/bin/env bash set -euo pipefail echo "Running unit tests..." npm test -- --passWithNoTests --watchAll=false --silent echo "✓ All tests passed"
prepare-commit-msg
prepare-commit-msg runs after the default commit message template is set up but before the editor is shown to the user. It receives three arguments:
Argument | Value |
|---|---|
| Path to the file containing the commit message (e.g., |
| Type of commit: |
| SHA of the commit being amended (only present when |
.git/hooks/prepare-commit-msg — auto-prepend branch name
#!/usr/bin/env bash
# Prepend the Jira ticket number from the branch name to the commit message.
# Branch pattern: feature/PROJ-1234-some-description
# Result: "[PROJ-1234] " prepended to every commit message
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"
# Don't modify merge commits, amends, or squash messages
if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] || [ "$COMMIT_SOURCE" = "commit" ]; then
exit 0
fi
# Extract ticket from branch name
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || true)
TICKET=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+' | head -1 || true)
if [ -n "$TICKET" ]; then
# Prepend ticket only if the message doesn't already contain it
if ! grep -qF "[$TICKET]" "$COMMIT_MSG_FILE"; then
sed -i.bak "1s/^/[$TICKET] /" "$COMMIT_MSG_FILE"
fi
ficommit-msg
commit-msg receives one argument: the path to the file that contains the commit message the user typed. It can validate, modify, or reject the message. This is the ideal place to enforce Conventional Commits or any house style.
.git/hooks/commit-msg — enforce Conventional Commits
#!/usr/bin/env bash
set -euo pipefail
COMMIT_MSG_FILE="$1"
MSG=$(cat "$COMMIT_MSG_FILE")
# Conventional Commits pattern:
# type(optional-scope): description
PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)((.+))?: .{1,72}$'
# Ignore merge commits, fixup!, squash! prefixes
if echo "$MSG" | grep -qE '^(Merge|Revert|fixup!|squash!)'; then
exit 0
fi
# Check first line
FIRST_LINE=$(echo "$MSG" | head -1)
if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then
echo ""
echo "✗ Invalid commit message format."
echo ""
echo " Your message: $FIRST_LINE"
echo ""
echo " Required format: <type>(scope): <description>"
echo " Examples:"
echo " feat(auth): add OAuth2 login flow"
echo " fix: prevent crash on empty user list"
echo " docs: update API reference for /users endpoint"
echo ""
echo " Valid types: feat, fix, docs, style, refactor, perf,"
echo " test, build, ci, chore, revert"
echo ""
exit 1
fi
# Check that the description doesn't end with a period
if echo "$FIRST_LINE" | grep -qE '.$'; then
echo "✗ Commit message should not end with a period."
exit 1
fi
exit 0Example rejection output
✗ Invalid commit message format.
Your message: Added login button
Required format: <type>(scope): <description>
Examples:
feat(auth): add OAuth2 login flow
fix: prevent crash on empty user list
Valid types: feat, fix, docs, style, refactor, perf,
test, build, ci, chore, revertpost-commit
post-commit fires immediately after a commit is successfully created. It cannot abort anything — it is purely informational. Common uses: notifications, logging, updating a local issue tracker.
.git/hooks/post-commit — show a summary notification
#!/usr/bin/env bash # Show a macOS notification with the commit summary HASH=$(git rev-parse --short HEAD) MSG=$(git log -1 --pretty=%s) BRANCH=$(git symbolic-ref --short HEAD) osascript -e "display notification "$MSG" with title "Git commit $HASH" subtitle "$BRANCH"" # On Linux with libnotify: # notify-send "Git commit $HASH" "$MSG"
pre-push
pre-push runs before Git transfers objects to the remote. It receives the remote name and URL as arguments, and reads a list of refs to be pushed from stdin (one per line: <local-ref> <local-sha> <remote-ref> <remote-sha>). Use this as a final gate — run the full test suite or security checks.
.git/hooks/pre-push — block pushing to main, run tests
#!/usr/bin/env bash
set -euo pipefail
REMOTE="$1"
URL="$2"
# Block direct pushes to main/master
while read local_ref local_sha remote_ref remote_sha; do
if [[ "$remote_ref" == "refs/heads/main" || "$remote_ref" == "refs/heads/master" ]]; then
echo "✗ Direct pushes to 'main' or 'master' are not allowed."
echo " Please open a pull request."
exit 1
fi
done
# Run full test suite before any push
echo "Running test suite before push..."
npm test -- --watchAll=false
echo "✓ Tests passed. Pushing...".git/hooks/pre-push — check for secrets with gitleaks
#!/usr/bin/env bash set -euo pipefail # Requires gitleaks: https://github.com/gitleaks/gitleaks if command -v gitleaks &>/dev/null; then echo "Scanning for secrets with gitleaks..." gitleaks protect --staged --redact --verbose echo "✓ No secrets detected." fi
post-checkout
post-checkout fires after git checkout or git switch completes. It receives three arguments: <previous-HEAD> <new-HEAD> <is-branch-checkout> (1 if a branch was checked out, 0 if a file was checked out). Use it to install dependencies when the branch changes.
.git/hooks/post-checkout — auto-install when package.json changes
#!/usr/bin/env bash PREV_HEAD="$1" NEW_HEAD="$2" IS_BRANCH_CHECKOUT="$3" # Only run on branch checkouts, not file checkouts if [ "$IS_BRANCH_CHECKOUT" = "0" ]; then exit 0 fi # Check if package.json changed between the old and new branch if git diff --name-only "$PREV_HEAD" "$NEW_HEAD" | grep -q "package.json"; then echo "package.json changed. Running npm install..." npm install echo "✓ Dependencies updated." fi
post-merge
post-merge fires after a successful git merge. It receives one argument: 1 if the merge was a squash, 0 if it was a normal merge. Use it similarly to post-checkout to sync dependencies.
.git/hooks/post-merge — sync dependencies after merge
#!/usr/bin/env bash # Check if package-lock.json changed during the merge if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "package-lock.json"; then echo "package-lock.json changed in merge. Running npm ci..." npm ci echo "✓ Dependencies synced." fi
pre-rebase
pre-rebase runs before a rebase starts. It receives the upstream (first argument) and the branch being rebased (second, if not the current). Use it to prevent rebasing shared branches or to warn about dangerous operations.
.git/hooks/pre-rebase — prevent rebasing pushed branches
#!/usr/bin/env bash
UPSTREAM="$1"
BRANCH="${2:-$(git symbolic-ref --short HEAD)}"
# Prevent rebasing main or release branches
PROTECTED_BRANCHES="main master release/"
for protected in $PROTECTED_BRANCHES; do
if [[ "$BRANCH" == "$protected"* ]]; then
echo "✗ Rebasing '$BRANCH' is not allowed (protected branch)."
exit 1
fi
done
# Warn if the branch has been pushed
REMOTE_EXISTS=$(git ls-remote --exit-code --heads origin "$BRANCH" 2>/dev/null || echo "")
if [ -n "$REMOTE_EXISTS" ]; then
echo "⚠ Warning: '$BRANCH' has been pushed to origin."
echo " Rebasing will rewrite history. Others may already have this branch."
read -p " Continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fiQuick reference
Hook | Arguments | Can abort | Primary use |
|---|---|---|---|
| None | Yes | Lint, format, fast tests on staged files |
|
| Yes | Auto-populate commit message template |
|
| Yes | Validate commit message format |
| None | No | Notifications, logging |
|
| Yes | Full test suite, secret scanning, branch protection |
|
| No | Install deps, set up env per branch |
|
| No | Sync deps after merge |
|
| Yes | Protect shared branches from rebase |
| None | Yes | Final check before merge commit is created |