Git Hooks: Automate Your Development Workflow
In this tutorial, you'll learn about Git Hooks: Automate Your Development Workflow. We cover key concepts, practical examples, and best practices.
Git hooks are executable scripts that Git triggers on committing, pushing, or merging to enforce code quality and catch issues before they reach your repository.
In this tutorial, you'll learn Git hooks — scripts that run automatically at key points in Git's workflow. Hooks let you enforce code quality, run tests, and catch issues before they ever reach your repository. By the end, you'll write custom hooks and use popular hook management frameworks.
flowchart LR
A[git commit] --> B[pre-commit hook]
B --> C{Pass?}
C -->|No| D[Hook aborts commit]
C -->|Yes| E[Commit message editor]
E --> F[commit-msg hook]
F --> G{Pass?}
G -->|No| H[Hook aborts commit]
G -->|Yes| I[post-commit hook]
I --> J[Commit recorded]
Client-Side vs Server-Side Hooks
| Hook Type | Location | Trigger | Common Uses |
|---|---|---|---|
| pre-commit | Local | Before commit message | Lint, format, check secrets |
| prepare-commit-msg | Local | Before commit editor opens | Auto-generate messages |
| commit-msg | Local | After message is written | Validate message format |
| post-commit | Local | After commit completes | Notify, log |
| pre-push | Local | Before push | Run tests, check branches |
| pre-receive | Server | Before server accepts push | Enforce policies |
| update | Server | Per-branch on push | Branch-specific rules |
| post-receive | Server | After push accepted | Deploy, notify |
Writing a Pre-Commit Hook
Create a pre-commit hook that prevents committing debug statements:
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
if git diff --cached --name-only | grep -q "\.js$"; then
if git diff --cached | grep -q "debugger\|console\.log"; then
echo "ERROR: Remove debugger/console.log before committing"
exit 1
fi
fi
echo "Pre-commit checks passed"
exit 0
Make it executable:
chmod +x .git/hooks/pre-commit
Now try to commit a file with console.log:
echo "console.log('test')" > app.js
git add app.js
git commit -m "Add app.js"
Expected output:
Running pre-commit checks...
ERROR: Remove debugger/console.log before committing
Writing a Commit-Msg Hook
Validate commit messages follow a conventional format:
#!/bin/bash
# .git/hooks/commit-msg
# Pattern: type(scope): description
pattern="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}$"
if ! grep -qE "$pattern" "$1"; then
echo "ERROR: Commit message must match: type(scope): description"
echo "Example: feat(login): add OAuth support"
exit 1
fi
exit 0
Using Hook Management Tools
Shared hooks via a tools directory:
# Create a hooks directory in your repo
mkdir -p .githooks
git config core.hooksPath .githooks
Now all hooks in .githooks/ are part of your repository and shared across clones.
Server-Side Hooks
Server-side hooks run on the Git server during push operations:
| Hook | Trigger | Use Case |
|---|---|---|
| pre-receive | Before any refs are updated | Enforce project-wide policies |
| update | Per-ref, before each branch/tag update | Branch-specific rules |
| post-receive | After all refs are updated | Deploy, notify, trigger CI |
Sample pre-receive hook that blocks non-fast-forward pushes:
#!/bin/bash
# Server-side pre-receive hook
while read oldrev newrev refname; do
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
# Check if this is a force push (non-fast-forward)
if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
echo "ERROR: Non-fast-forward push rejected for $refname"
exit 1
fi
fi
done
Pre-Push Hook for Testing
Run tests before pushing:
#!/bin/bash
# .githooks/pre-push
echo "Running tests before push..."
npm test
if [ $? -ne 0 ]; then
echo "ERROR: Tests failed. Push aborted."
exit 1
fi
Expected output on failure:
Running tests before push...
npm test failed: 1 test failing
ERROR: Tests failed. Push aborted.
Common Errors
| Error | Cause | Fix |
|---|---|---|
| Hook ignored | Not executable | chmod +x .git/hooks/hook-name |
Permission denied |
Wrong shebang line | Use #!/bin/bash or #!/usr/bin/env bash |
| Hook runs despite failure | Missing exit 1 |
Always return non-zero on failure |
| Hook not triggered | Wrong filename | Name must be exact: pre-commit, not precommit |
| Skipped hooks | --no-verify flag used |
Educate team not to skip |
| Hook pulls image every time | Docker-based hooks inefficient | Use host tools or cache Docker images |
core.hooksPath ignored |
Old Git version | Requires Git 2.9+ |
| Binary file conflicts | Hooks checked in as .sample |
Remove .sample extension |
Practice Questions
Challenge
Write a pre-commit hook that checks for hardcoded API keys, passwords, and tokens in staged files. Use regex patterns to detect API_KEY=, password=, and secret =. Block the commit if any match. Also write a commit-msg hook that enforces the Conventional Commits format. Create both hooks and test them with sample files.
Real-World Task
In a team repository managed by GitHub, implement a pre-commit hook that runs a linting tool (like ESLint for JavaScript or flake8 for Python) and a pre-push hook that runs the full test suite. Store hooks in .githooks/ and configure core.hooksPath. Push the hooks directory so all team members get the same checks. This setup is used at DodaTech for DodaZIP development to ensure every commit meets quality standards.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro