Writing a Hook

Updated

October 30, 2026

Writing Your First Hook

After this page you have a pre-commit that refuses a commit containing a tab-only whitespace error, and a commit-msg that checks a conventional subject.

pre-commit

cat > ~/desk/desk-api/.git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
git diff --cached --check
EOF
chmod +x ~/desk/desk-api/.git/hooks/pre-commit

git diff --cached --check flags leftover conflict markers and bad whitespace in the index. Exit status non-zero aborts the commit.

Try:

cd ~/desk/desk-api
printf 'bad \n' >> README.md    # trailing space
git add README.md
git commit -m "docs: trigger hook"

Fix the line, git add, commit again.

commit-msg

Git passes the path to a temporary file that holds the message:

cat > ~/desk/desk-api/.git/hooks/commit-msg <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
file="$1"
subject="$(head -n1 "$file")"
if [[ "$subject" == WIP* ]]; then
  echo "commit-msg: subject cannot start with WIP" >&2
  exit 1
fi
EOF
chmod +x ~/desk/desk-api/.git/hooks/commit-msg

A stricter conventional check can wait until the team agrees on types. Start by blocking WIP.

pre-push

Git passes the remote name and URL, and on stdin: local ref, local hash, remote ref, remote hash.

cat > ~/desk/desk-api/.git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
while read -r local_ref local_sha remote_ref remote_sha; do
  if [[ "$remote_ref" == refs/heads/main && "$local_sha" != "$remote_sha" ]]; then
    echo "pre-push: running tests before updating main" >&2
    # python -m pytest -q || exit 1
  fi
done
EOF
chmod +x ~/desk/desk-api/.git/hooks/pre-push

Uncomment the test command when desk-api has tests.

--no-verify

git commit --no-verify -m "docs: skip hooks"

Emergency only. Treat it as breaking the seatbelt.