Git Hooks & Automation

Reviewed & published by Brayan K

Let Git run your checks for you. By the end of this lesson you'll write a pre-commit hook that blocks bad commits, a commit-msg hook that enforces a message format, and you'll know how Husky shares hooks with your whole team.

Part of the free Git course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ Where Hooks Live

Every repo has a .git/hooks directory full of .sample scripts Git ignores. To activate one, create a file with the real hook name (drop .sample) and make it executable. The client-side hooks you'll use most are pre-commit, commit-msg, and pre-push.

# A "hook" is a script Git runs automatically at a certain moment — like
# right before a commit. They live in the .git/hooks directory.

ls .git/hooks

# Git ships SAMPLE hooks ending in .sample (which it ignores). To activate
# one, create a file with the real hook name (no .sample) and make it
# executable. Client-side hooks you'll meet most:
#   pre-commit   runs BEFORE the commit is created (lint, format, quick tests)
#   commit-msg   validates/edits the commit MESSAGE
#   pre-push     runs BEFORE a push (good place for the full test suite)

2️⃣ A pre-commit Hook

The pre-commit hook runs before the commit is recorded — perfect for linting, formatting, or quick checks. The rule is simple: exit non-zero to abort the commit, exit 0 to allow it. Here's one that refuses any commit whose staged changes contain TODO.

# A pre-commit hook that BLOCKS any commit containing the word TODO.
# Save this as .git/hooks/pre-commit (no extension) and make it executable.

#!/bin/sh
# Check the staged changes for "TODO"
if git diff --cached | grep -q "TODO"; then
  echo "Error: remove TODO before committing"
  exit 1            # non-zero exit ABORTS the commit
fi
exit 0              # zero exit lets the commit proceed

# Make it runnable, then test it:
#   chmod +x .git/hooks/pre-commit
#   git commit -m "test"   # blocked if any staged line contains TODO

3️⃣ A commit-msg Hook

The commit-msg hook receives the path to the message file as its first argument, so it can validate or reject the wording. This one insists every message references a ticket like JIRA-123.

# A commit-msg hook that REQUIRES a ticket reference like "JIRA-123".
# Git passes the path to the message file as the first argument ($1).
# Save as .git/hooks/commit-msg and chmod +x it.

#!/bin/sh
if ! grep -qE "[A-Z]+-[0-9]+" "$1"; then
  echo "Error: commit message must reference a ticket, e.g. JIRA-123"
  exit 1
fi

# Now "git commit -m 'fix bug'" is rejected, but
# "git commit -m 'JIRA-42 fix bug'" is accepted.

Your turn. Write a pre-commit hook that blocks the word DEBUG. Fill in the three blanks.

# 🎯 YOUR TURN — replace each ___ then test it in a practice repo.
# Goal: a pre-commit hook that blocks the word DEBUG.

# Save this as .git/hooks/pre-commit:
#!/bin/sh
if git diff --cached | grep -q "DEBUG"; then
  echo "Error: remove DEBUG before committing"
  ___ 1            # 👉 the keyword that aborts the commit (non-zero)
fi
___ 0              # 👉 the same keyword with 0 to allow the commit

# Then make it runnable:
chmod ___ .git/hooks/pre-commit   # 👉 the flag that adds execute permission

# ✅ Expected: committing a staged line containing DEBUG is blocked;
#    a clean commit goes through.

4️⃣ Sharing Hooks with Your Team

Here's the catch: .git/hooks isn't committed, so your hooks don't travel with clones — teammates won't have them. Tools solve this by keeping hooks in the tracked part of the repo. Husky stores them under .husky/ for JavaScript projects; the language-agnostic pre-commit framework uses a committed .pre-commit-config.yaml.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference

Hook / CommandPurpose
.git/hooks/pre-commitRun before a commit (lint/format)
.git/hooks/commit-msgValidate the commit message ($1)
.git/hooks/pre-pushRun before a push (tests)
chmod +x <hook>Make a hook runnable
git commit --no-verifySkip hooks for one commit
npx husky initSet up shared hooks (JS)

Mini-Challenge: Enforce Quality Before Every Commit

No full scripts this time — just the plan. Build hooks that catch bad code and bad messages, then share them.

# 🎯 MINI-CHALLENGE: enforce quality before every commit.
# No full scripts given — just the plan. Use a practice repo.
#
# 1. Create .git/hooks/pre-commit that runs your linter or test command
# 2. chmod +x it, then commit something that fails the check — confirm it's blocked
# 3. Fix the issue and confirm a clean commit succeeds
# 4. Add a commit-msg hook that rejects empty or one-word messages
# 5. Bonus: move your hook into Husky (.husky/) so the team shares it
#
# ✅ Expected: bad code and bad messages are blocked locally before they
#    ever reach the repo, and the hook can be shared via Husky.

# your commands here

🎉 Lesson Complete!

Practice quiz

What is a Git hook?

  • A remote branch
  • A merge conflict marker
  • A script Git runs automatically at certain points, like before a commit
  • A type of tag

Answer: A script Git runs automatically at certain points, like before a commit. Hooks are scripts Git runs on events such as committing or pushing, to automate or enforce steps.

Where do a repository's hooks live by default?

  • .git/hooks
  • .github/hooks
  • .hooks
  • the repo root

Answer: .git/hooks. Each repo's hooks live in the .git/hooks directory as executable scripts.

Which hook runs BEFORE a commit is created, often to lint or test?

  • post-commit
  • pre-push
  • commit-msg
  • pre-commit

Answer: pre-commit. pre-commit runs before the commit is recorded; a non-zero exit aborts the commit.

How does a pre-commit hook block a commit?

  • By printing 'STOP'
  • By exiting with a non-zero status code
  • By deleting the file
  • By returning true

Answer: By exiting with a non-zero status code. If the hook exits non-zero, Git aborts the commit; exit 0 lets it proceed.

Which hook can validate or reformat the commit MESSAGE?

  • commit-msg
  • pre-commit
  • post-checkout
  • pre-rebase

Answer: commit-msg. commit-msg receives the path to the message file and can check or reject it.

Why are hooks in .git/hooks NOT shared with teammates by default?

  • They are encrypted
  • They are too large
  • The .git directory isn't committed or cloned
  • Git deletes them on push

Answer: The .git directory isn't committed or cloned. The .git directory isn't part of the tracked tree, so its hooks don't travel with clones.

What must a hook script be in order to run?

  • Named .hook
  • Executable (and have a valid shebang)
  • Written in Python
  • Committed to main

Answer: Executable (and have a valid shebang). Git only runs hook files that are executable; a shebang like #!/bin/sh tells the OS how to run it.

What problem does a tool like Husky solve?

  • It speeds up cloning
  • It encrypts commits
  • It hosts repos
  • It version-controls and installs hooks so the whole team shares them

Answer: It version-controls and installs hooks so the whole team shares them. Husky keeps hooks in the repo and wires them up on install, so everyone gets the same checks.

The 'pre-commit' framework is configured with which file?

  • hooks.json
  • .pre-commit-config.yaml
  • .gitconfig
  • Makefile

Answer: .pre-commit-config.yaml. The pre-commit framework reads .pre-commit-config.yaml to manage multi-language hooks.

Which hook runs on the developer's machine just before code is sent to the remote?

  • post-receive
  • update
  • pre-push
  • post-merge

Answer: pre-push. pre-push runs locally before a push completes — handy for running the test suite first.

Continue this course

Frequently asked questions

What exactly is a Git hook?

A hook is a script Git runs automatically at a specific point in its workflow — for example just before a commit (pre-commit), when checking the commit message (commit-msg), or before a push (pre-push). They let you automate checks and chores: run a linter, format code, run quick tests, or enforce a message convention. If a hook exits with a non-zero status, Git aborts the action.

Where do hooks live, and why aren't they shared?

Each repo's hooks live in its .git/hooks directory as executable scripts. The .git directory is local plumbing — it isn't part of the tracked file tree, so it's never committed, pushed, or included in a clone. That means hooks you set up by hand exist only on your machine; teammates won't get them unless you use a tool that stores hooks in the tracked part of the repo.

How does a hook block an action like a commit?

By its exit code. Git runs the hook and inspects the result: exit 0 means 'all good, proceed', and any non-zero exit means 'stop'. So a pre-commit script that finds a problem prints an error and runs exit 1, which aborts the commit. This simple convention is how every client-side enforcement hook works.

Why do teams use Husky or the pre-commit framework?

Because raw .git/hooks scripts aren't shared. Husky (popular in JavaScript projects) keeps hooks under a tracked .husky/ folder and installs them automatically, so everyone who clones gets the same checks. The language-agnostic 'pre-commit' framework does the same via a committed .pre-commit-config.yaml and a large library of ready-made hooks. Both solve the 'works on my machine only' problem.

Are hooks a security guarantee?

No. Client-side hooks run on the developer's own machine and can be bypassed (git commit --no-verify) or simply not installed. They're great for catching mistakes early and keeping a team consistent, but they're conveniences, not enforcement. For guarantees, pair them with server-side checks — required status checks in CI (GitHub Actions) or protected-branch rules that can't be skipped locally.