Git Hooks: Preventing Pipeline Failures with Pre-Push ValidationΒΆ
This tutorial explains how to use Git hooks to catch errors locally before pushing code, preventing CI/CD pipeline failures and saving time in the development workflow.
What Are Git Hooks?ΒΆ
Git hooks are scripts that Git automatically executes before or after certain events such as commit, push, or merge. They allow you to:
- Run automated checks before code leaves your local machine
- Enforce code quality standards
- Prevent broken code from reaching the remote repository
- Save CI/CD pipeline minutes and reduce feedback loops
Git Hooks vs. MkDocs Hooks
Don't confuse Git hooks (scripts that run during Git operations) with MkDocs hooks (Python functions that run during documentation builds). See MkDocs Hooks and JupyterLite Images for details about MkDocs hooks.
Common Git Hook TypesΒΆ
| Hook Name | When It Runs | Common Use Cases |
|---|---|---|
pre-commit |
Before a commit is created | Lint code, format check, run quick tests |
commit-msg |
After commit message is entered | Validate commit message format |
on_config |
After config loads | Modify configuration dynamically |
on_pre_build |
Before build starts | Clean directories, prepare assets |
on_startup |
Before anything else | Initialize resources, check dependencies |
on_files |
When files are collected | Add/remove/modify file list |
on_post_build |
After build completes | Copy assets, post-process files β We use this! |
on_page_markdown |
Before markdown rendering | Transform markdown content |
on_page_content |
After markdown to HTML | Modify HTML content |
pre-push |
Before pushing to remote | Run builds, comprehensive tests, validate deployability |
post-merge |
After a merge completes | Update dependencies, rebuild assets |
Why Use Pre-Push Hooks?ΒΆ
The Problem: Delayed FeedbackΒΆ
Without pre-push validation:
ββββββββββββ βββββββββββββ ββββββββββ βββββββββββββββ
βDeveloper β β Local Git β β GitHub β β CI Pipeline β
βββββββ¬βββββ βββββββ¬ββββββ βββββ¬βββββ ββββββββ¬βββββββ
β β β β
β 1. git commit β β β
ββββββββββββββββββ>β β β
β β β β
β 2. git push β β β
ββββββββββββββββββ>β β β
β β 3. Push code β β
β ββββββββββββββββ>β β
β β β 4. Trigger build β
β β ββββββββββββββββββ>β
β β β β
β β β Build runs β
β β β (2-10 minutes) β
β β β β
β β 5. β Build failed! β
β <βββββββββββββββββ΄βββββββββββββββββ΄βββββββββββββββββββ€
β β
β π Wasted 5-20 minutes waiting for CI... β
β β
β 6. Fix, commit, push again β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ>β
β β
β Build runs again (2-10 minutes) β
β β
β 7. β
Success (finally!) β
β <βββββββββββββββββββββββββββββββββββββββββββββββββββ€ β
β β
View Interactive Sequence Diagram
sequenceDiagram
participant Dev as Developer
participant Git as Local Git
participant GH as GitHub
participant CI as CI Pipeline
Dev->>Git: git commit
Dev->>Git: git push
Git->>GH: Push code
GH->>CI: Trigger pipeline
Note over CI: Build runs (2-10 min)
CI->>Dev: β Build failed!
Note over Dev: π Wasted 5-20 minutes
Dev->>Git: Fix, commit, push
Git->>GH: Push again
GH->>CI: Trigger pipeline again
Note over CI: Build runs (2-10 min)
CI->>Dev: β
Success (finally)
Time wasted: 5-20+ minutes per iteration
The Solution: Pre-Push ValidationΒΆ
With a pre-push hook:
ββββββββββββ βββββββββββββββ βββββββββββββ ββββββββββ βββββββββββββββ
βDeveloper β β Pre-Push β β Local Git β β GitHub β β CI Pipeline β
β β β Hook β β β β β β β
βββββββ¬βββββ ββββββββ¬βββββββ βββββββ¬ββββββ βββββ¬βββββ ββββββββ¬βββββββ
β β β β β
β 1. git commit β β β β
βββββββββββββββββββββββββββββββββββββ> β β
β β β β β
β 2. git push β β β β
βββββββββββββββββββββββββββββββββββββ> β β
β β 3. Run hook β β β
β β<βββββββββββββββββ€ β β
β β β β β
β β Build check β β β
β β (10-60 sec) β β β
β β β β β
β 4. β Error! β β β β
β<βββββββββββββββββ€ β β β
β β β β β
β β
Fix immediately (seconds, not minutes!) β β
β β β β β
β 5. git commit & push again β β β
βββββββββββββββββββββββββββββββββββββ> β β
β β 6. Run hook β β β
β β<βββββββββββββββββ€ β β
β β Build check β β β
β β (10-60 sec) β β β
β β β β β
β 7. β
Passed! β β β β
β<βββββββββββββββββ€ β β β
β β β 8. Push code β β
β β ββββββββββββββ>β β
β β β β 9. Trigger CI β
β β β ββββββββββββββββ>β
β β β β β
β 10. β
CI Success (first try!) β β β
β <ββββββββββββββββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββ€
β β β β β
View Interactive Sequence Diagram
sequenceDiagram
participant Dev as Developer
participant Hook as Pre-Push Hook
participant Git as Local Git
participant GH as GitHub
participant CI as CI Pipeline
Dev->>Git: git commit
Dev->>Git: git push
Git->>Hook: Run validation
Note over Hook: Build check (10-60 sec)
Hook->>Dev: β Error found!
Note over Dev: Fix immediately
Dev->>Git: Fix, commit, push
Git->>Hook: Run validation
Note over Hook: Build check (10-60 sec)
Hook->>Git: β
Passed!
Git->>GH: Push code
GH->>CI: Trigger pipeline
CI->>Dev: β
Success!
Time saved: 90% reduction in CI failures and wait time
Real Example: MkDocs Build ValidationΒΆ
The ScenarioΒΆ
Your GitHub Actions pipeline runs:
If this fails in CI, you've wasted:
- Pipeline execution time
- Your own time waiting for results
- Potential embarrassment if others are watching the repo
The Hook SolutionΒΆ
A pre-push hook that runs the exact same command locally:
| .git/hooks/pre-push | |
|---|---|
- Shebang line - Required as the first line to tell the system this is a Bash script
- Negated condition -
!means "if this command fails", so we enter the block on failure - Exit code 1 - Non-zero exit code tells Git to abort the push operation
Automatic Execution
Now when you git push, this runs automatically and blocks the push if it fails!
How Git Hooks Work: ArchitectureΒΆ
Where Hooks LiveΒΆ
your-project/
βββ .git/
β βββ hooks/ # (1)!
β βββ pre-commit.sample
β βββ pre-push.sample
β βββ pre-push # (2)!
β βββ ...
βββ src/
βββ docs/
βββ pyproject.toml
- Git hooks directory - Created automatically when you run
git init - Your custom hook - Remove the
.sampleextension to activate
Not Version Controlled
Files in .git/ are never committed to version control. See Sharing Hooks with Your Team for solutions.
Hook Execution FlowΒΆ
ββββββββββββββββββββββββββββββ
β Developer runs: git push β
ββββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Hook exists at β
β .git/hooks/pre-push? β
βββββββββ¬βββββββββββββββ¬ββββββββββββ
β β
NOβ βYES
β β
βΌ βΌ
βββββββββββββββββββββββ ββββββββββββββββββββββ
β Proceed with push β β Execute hook β
β immediately β β script β
βββββββββββββββββββββββ βββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Exit code = 0? β
β (Success?) β
ββββββ¬βββββββββββββββ¬ββββ
β β
YES β β NO
β β
βΌ βΌ
βββββββββββββββββββ ββββββββββββββββββββ
β β
Continue β β β BLOCK push β
β with push β β Show error msg β
βββββββββββββββββββ ββββββββββββββββββββ
View Interactive Flowchart
flowchart TD
A[Developer runs: git push] --> B{Hook exists at<br/>.git/hooks/pre-push?}
B -->|No| C[Proceed with push immediately]
B -->|Yes| D[Execute hook script]
D --> E{Exit code = 0?}
E -->|Yes - Success| F[Continue with push]
E -->|No - Failure| G[BLOCK push<br/>Show error message]
style F fill:#90EE90
style G fill:#FFB6C6
style D fill:#87CEEB
Exit Codes MatterΒΆ
Hooks communicate success/failure through exit codes:
- Exit 0: Success β Git continues with the operation
- Exit 1-255: Failure β Git aborts the operation
Implementing Pre-Push HooksΒΆ
Step 1: Create the Hook FileΒΆ
Navigate to your repository:
Step 2: Write the Hook ScriptΒΆ
For MkDocs validation:
set -ecauses the script to exit immediately if any command fails (except in if conditions)- Exact CI command - Run the identical command that runs in your GitHub Actions workflow
- Exit 1 blocks the push when validation fails
- Exit 0 allows the push to proceed when validation succeeds
Match Your CI Exactly
Always use the exact same command that runs in your CI pipeline to ensure 100% parity between local and remote validation.
Step 3: Make the Hook ExecutableΒΆ
Required Step
Hooks must be executable or Git will silently ignore them!
Step 4: Test the HookΒΆ
Try pushing to test:
You should see output like:
π Running pre-push validation...
π Building MkDocs site...
INFO - Cleaning site directory
INFO - Building documentation to directory: site
INFO - Documentation built in 2.34 seconds
β
All validations passed! Proceeding with push...
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Example: Intentional Failure Test
To test that your hook properly blocks pushes, temporarily break your mkdocs.yml:
Then try to push:
$ git push origin documentation
π Running pre-push validation...
π Building MkDocs site...
WARNING - Config value 'invalid_key': Unrecognised configuration name: invalid_key
Aborted with 1 configuration warnings in 'strict' mode!
β MkDocs build failed!
Fix the errors above and try pushing again.
error: failed to push some refs to 'origin'
Remove the invalid line and the push will succeed! β
Advanced Hook PatternsΒΆ
Multiple Validation StepsΒΆ
Run several checks in sequence:
- Each validation step fails fast - if linting fails, we don't waste time running tests
Performance Consideration
Each check adds time to your push. Keep total hook execution under 60 seconds for good developer experience.
Conditional Checks (Only Changed Files)ΒΆ
Optimize by only checking files you've changed:
- Compare your branch to the remote main branch to find what changed
- Regex pattern matches files in
docs/directory or themkdocs.ymlfile
When to Use Conditional Checks
Use this pattern when:
- Your validation is slow (> 30 seconds)
- Changes to unrelated files don't affect the check
- Your repo has multiple subsystems (docs, code, tests, etc.)
Interactive Override (Emergency Bypass)ΒΆ
- The
--no-verifyflag skips all hooks for this push
Emergency Use Only
Only use this when:
- You're absolutely certain the code is safe
- There's a critical production issue
- The hook itself is broken (rare)
Never use this to bypass failing validations without fixing them!
Sharing Hooks with Your TeamΒΆ
The ChallengeΒΆ
Git hooks in .git/hooks/ are not tracked by version control. Each team member needs to install them manually.
βββββββββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββββ
β Developer 1 β β GitHub Repo β β Developer 2 β
β β
Has hook βββββ pushes ββββ> β β ββββ clones βββ> β β NO hook β
β installed β β β β installed β
βββββββββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββββ
(Protected) (Vulnerable)
View Interactive Diagram
Solution 1: Tracked Hook ScriptsΒΆ
Store hook scripts in a tracked directory:
your-project/
βββ .git/
β βββ hooks/ # β Not tracked (Git internal)
βββ .githooks/ # β Tracked! β
Commit this!
β βββ pre-push
β βββ pre-commit
β βββ install.sh
βββ docs/
βββ pyproject.toml
Install script (.githooks/install.sh):
- Skip copying the install script itself - only copy actual hook files
Setup instructions for team (add to README):
## Development Setup
After cloning this repository, install Git hooks:
```bash
bash .githooks/install.sh
This installs pre-push validation that catches errors before they reach CI.
!!! success "Commit the Hooks"
```bash
git add .githooks/
git commit -m "Add pre-push hook for MkDocs validation"
git push
```
Now everyone who clones can run the install script!
### Solution 2: Git Config (Git 2.9+)
Configure Git to use a tracked hooks directory automatically:
```bash
# Set hooks directory for this repo
git config core.hooksPath .githooks
Now hooks in .githooks/ are used automatically without manual installation!
Add to project setup:
| .githooks/pre-push | |
|---|---|
Commit it:
Team members setup:
Make it Easy for Your Team
Add hook setup to your project documentation and onboarding checklist so new team members don't forget!
Solution 3: Pre-Commit FrameworkΒΆ
Use the pre-commit framework for robust, cross-platform hook management:
Install:
Create configuration:
| .pre-commit-config.yaml | |
|---|---|
- Unique ID for this hook
- The command to run - same as your CI pipeline
- Don't pass changed filenames to the command
- Only run this hook on push, not commit
Install hooks:
Commit the config:
Automatic for Everyone
Team members just need to run pre-commit install --hook-type pre-push after cloning. The framework handles platform differences automatically!
TroubleshootingΒΆ
Hook Not RunningΒΆ
Problem: Push completes without running hook
Solution 1: Verify File Exists
Solution 3: Verify Shebang Line
The first line must be:
Or for sh compatibility:
Common mistakes:
- Missing shebang entirely
- Extra spaces before
# - Not on the first line
Solution 4: Test Hook Manually
Hook Blocks Valid PushΒΆ
Problem: Hook fails but code is actually fine
Solution 1: Check Your Environment
Solution 2: Run Validation Manually
Solution 3: Check Working Directory
Hooks run from the repository root. Verify:
Solution 4: Emergency Bypass
If you're certain the code is fine:
But investigate why the hook failed afterwards!
Windows Line Ending IssuesΒΆ
Problem: Hook fails with ^M: bad interpreter error
This happens when the hook file has Windows line endings (CRLF) instead of Unix line endings (LF).
Solution 1: Using dos2unix (Git Bash)
Solution 2: Using PowerShell
Solution 3: Using Git Configuration
Configure Git to handle line endings automatically:
Hook Shows Error But Push ContinuesΒΆ
Problem: Hook prints error message but push still succeeds
The hook is not exiting with a non-zero code!
Solution: Add Explicit Exit
Wrong β:
if ! uv run mkdocs build --strict; then
echo "Error!"
# Missing exit 1 here!
fi
# Script continues and returns 0 by default
Correct β :
Or use set -e at the top:
Best PracticesΒΆ
1. Keep Hooks Fast β‘ΒΆ
Users will bypass slow hooks, defeating their purpose!
| Duration | User Experience |
|---|---|
| < 10 seconds | Excellent - barely noticeable |
| 10-30 seconds | Good - acceptable wait time |
| 30-60 seconds | Poor - users get impatient |
| > 60 seconds | Terrible - users will bypass |
Speed Optimization Strategies
- Run only on changed files (see Conditional Checks)
- Use caching (UV caches dependencies automatically)
- Move slow checks to CI only (keep hooks for fast checks)
- Run checks in parallel when possible
2. Provide Clear Error Messages π¬ΒΆ
Bad β:
Good β :
echo "β MkDocs build failed!"
echo ""
echo "Common fixes:"
echo " β’ Check mkdocs.yml for configuration errors"
echo " β’ Validate markdown syntax in changed files"
echo " β’ Run 'uv run mkdocs build --strict' to see details"
echo ""
echo "Need help? See: docs/tutorials/git-hooks-pre-push-validation.md"
exit 1
Help Users Help Themselves
Good error messages include:
- What failed (specific check name)
- Why it might have failed (common causes)
- How to debug (command to run manually)
- Where to get help (documentation link)
3. Match CI Exactly π―ΒΆ
Run the exact same commands as your CI pipeline:
This ensures 100% parity between local and remote validation.
Don't Use Approximations
β Don't run mkdocs build locally if CI runs mkdocs build --strict
β Don't run tests on one Python version if CI uses another
β
Match the command exactly, including all flags and options
4. Document Hook Requirements πΒΆ
Add to your project README:
## Development Setup
### Install Git Hooks
This project uses Git hooks to validate code before pushing.
**Install hooks:**
```bash
bash .githooks/install.sh
```
**Required tools:**
- [UV](https://docs.astral.sh/uv/) (Python package manager)
- Python 3.12+
- Make (optional, for build automation)
**What the hook does:**
- Validates MkDocs can build without errors
- Catches configuration issues before CI
- Saves time by providing immediate feedback
**Bypass in emergencies only:**
```bash
git push --no-verify # NOT RECOMMENDED
```
This skips validation and should only be used for critical fixes.
5. Version Control Your Hooks ποΈΒΆ
Store hooks in .githooks/ or similar and commit them:
# Create tracked hooks directory
mkdir -p .githooks
# Move hook to tracked location
mv .git/hooks/pre-push .githooks/pre-push
# Make it executable
chmod +x .githooks/pre-push
# Commit to version control
git add .githooks/pre-push
git commit -m "Add pre-push hook for MkDocs validation"
# Configure Git to use this directory
git config core.hooksPath .githooks
Comparison: Hooks vs. CI-OnlyΒΆ
| Aspect | Pre-Push Hook | CI-Only |
|---|---|---|
| Feedback Speed | 10-60 seconds β‘ | 2-10+ minutes π |
| Catch Errors | Before push β | After push β |
| CI Load | Reduced π | Higher π |
| Developer Experience | Immediate feedback π | Delayed feedback π |
| Setup Complexity | Medium (manual install) | Low (automatic) |
| Bypassable | Yes (--no-verify) |
No (authoritative) |
| Platform Issues | Possible (Windows/Mac differences) | Rare (standardized environment) |
Best of Both Worlds
Use both:
- Pre-push hooks for fast, common checks (lint, quick tests, build validation)
- CI for comprehensive, authoritative validation (all tests, security scans, deployment)
Hooks catch 90% of issues instantly, CI provides final authority.
SummaryΒΆ
Git hooks, especially pre-push hooks, are powerful tools for:
β Catching errors early - Before they reach the remote repository β Saving time - Immediate local feedback vs. waiting for CI β Reducing CI load - Fewer failed pipeline runs β Enforcing standards - Consistent validation across the team
Quick ReferenceΒΆ
# Create pre-push hook
nano .git/hooks/pre-push
# Make executable
chmod +x .git/hooks/pre-push
# Test hook
git push origin your-branch
# Bypass hook (emergency only!)
git push --no-verify
Key TakeawaysΒΆ
- Hooks are scripts in
.git/hooks/that run automatically during Git operations - Pre-push hooks run before
git pushcompletes and can block it - Exit codes matter: 0 = success (allow), non-zero = failure (block)
- Share with your team by storing hooks in a tracked directory like
.githooks/ - Match CI exactly to ensure local validation mirrors remote validation
Next StepsΒΆ
-
Implement Your Hook
Follow the implementation guide to create a pre-push hook for your project
-
Test It
Intentionally break something and verify the hook blocks the push
-
Share with Team
Use Solution 2: Git Config or Solution 3: Pre-Commit Framework
-
Learn More
Explore advanced patterns for conditional checks and multi-step validation
Additional ResourcesΒΆ
- Git Hooks Official Documentation
- Pre-Commit Framework
- Atlassian Git Hooks Tutorial
- GitHub: Sample Git Hooks
Related Tutorials: