Git Hooks: Automate Quality Checks Before Code Leaves Your Machine

Git hooks are scripts that run automatically at specific points in your Git workflow. Use them to catch problems before they become PR comments. Here’s how to set them up effectively. Hook Basics Git hooks live in .git/hooks/. They’re executable scripts that run at specific events: 1 2 3 4 5 6 .git/hooks/ ├── pre-commit # Before commit is created ├── commit-msg # After commit message is entered ├── pre-push # Before push to remote ├── post-merge # After merge completes └── ... To enable a hook, create an executable script with the hook’s name: ...

March 1, 2026 Â· 5 min Â· 1056 words Â· Rob Washington

Git Workflow Patterns for Solo and Team Development

Git is powerful. It’s also easy to mess up. Here are workflows that keep repositories clean and teams productive. Solo Workflow For personal projects, keep it simple: 1 2 3 4 5 6 # Work on main, commit often git add -A git commit -m "Add user authentication" # Push when ready git push origin main Use branches for experiments: 1 2 3 4 git checkout -b experiment/new-ui # work... git checkout main git merge experiment/new-ui # or delete if failed Feature Branch Workflow The most common team pattern: ...

February 28, 2026 Â· 7 min Â· 1385 words Â· Rob Washington

Git Hooks: Automate Quality Before It's Too Late

Code review catches problems. CI catches problems. But the fastest feedback loop? Catching problems before you even commit. Git hooks run scripts at key points in your workflow. Use them to lint, test, and validate — automatically. Where Hooks Live 1 2 3 4 5 6 ls .git/hooks/ # applypatch-msg.sample pre-commit.sample # commit-msg.sample pre-push.sample # post-update.sample pre-rebase.sample # pre-applypatch.sample prepare-commit-msg.sample # pre-merge-commit.sample update.sample Remove .sample to activate. Hooks must be executable (chmod +x). ...

February 27, 2026 Â· 6 min Â· 1159 words Â· Rob Washington

Git Worktrees: Parallel Development Without the Branch-Switching Pain

Every developer knows the pain: you’re deep in a feature branch, someone asks you to review a PR, and now you’re stashing changes, switching branches, rebuilding dependencies, and losing your mental context. Git worktrees solve this elegantly. The Problem with Branch Switching Traditional git workflow assumes one working directory: 1 2 3 4 5 6 7 8 # You're working on feature-x git stash git checkout main git pull git checkout pr-branch npm install # dependencies changed npm run build # wait for it... # review, then reverse the whole process Every switch costs time and mental energy. Your editor loses state. Your dev server restarts. Your flow is gone. ...

February 26, 2026 Â· 4 min Â· 793 words Â· Rob Washington

Git Hooks for Automation: Enforce Quality Before It Hits the Repo

Git hooks run scripts at key points in your workflow. Use them to catch problems before they become pull requests. Hook Basics Hooks live in .git/hooks/. Each hook is an executable script named after the event. 1 2 3 4 5 6 # List available hooks ls .git/hooks/*.sample # Make a hook active cp .git/hooks/pre-commit.sample .git/hooks/pre-commit chmod +x .git/hooks/pre-commit Common Hooks Hook When Use Case pre-commit Before commit created Lint, format, test prepare-commit-msg After default message Add ticket numbers commit-msg After message entered Validate format pre-push Before push Run full tests post-merge After merge Install dependencies post-checkout After checkout Environment setup Pre-commit Hook Basic Linting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # .git/hooks/pre-commit echo "Running pre-commit checks..." # Get staged files STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) # Python files PYTHON_FILES=$(echo "$STAGED_FILES" | grep '\.py$') if [ -n "$PYTHON_FILES" ]; then echo "Linting Python files..." ruff check $PYTHON_FILES || exit 1 black --check $PYTHON_FILES || exit 1 fi # JavaScript files JS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|ts)$') if [ -n "$JS_FILES" ]; then echo "Linting JavaScript files..." eslint $JS_FILES || exit 1 fi echo "All checks passed!" Prevent Debug Statements 1 2 3 4 5 6 7 8 9 #!/bin/bash # .git/hooks/pre-commit # Check for debug statements if git diff --cached | grep -E '(console\.log|debugger|import pdb|breakpoint\(\))'; then echo "ERROR: Debug statements found!" echo "Remove console.log, debugger, pdb, or breakpoint() before committing." exit 1 fi Check for Secrets 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash # .git/hooks/pre-commit # Patterns that might be secrets PATTERNS=( 'password\s*=\s*["\047][^"\047]+' 'api_key\s*=\s*["\047][^"\047]+' 'secret\s*=\s*["\047][^"\047]+' 'AWS_SECRET_ACCESS_KEY' 'PRIVATE KEY' ) for pattern in "${PATTERNS[@]}"; do if git diff --cached | grep -iE "$pattern"; then echo "ERROR: Possible secret detected!" echo "Pattern: $pattern" exit 1 fi done Commit Message Hook Enforce Conventional Commits 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash # .git/hooks/commit-msg COMMIT_MSG_FILE=$1 COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") # Conventional commit pattern PATTERN="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}" if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then echo "ERROR: Invalid commit message format!" echo "" echo "Expected: <type>(<scope>): <description>" echo "Types: feat, fix, docs, style, refactor, test, chore" echo "" echo "Examples:" echo " feat(auth): add OAuth2 support" echo " fix: resolve memory leak in worker" echo "" exit 1 fi Add Ticket Number 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash # .git/hooks/prepare-commit-msg COMMIT_MSG_FILE=$1 BRANCH_NAME=$(git symbolic-ref --short HEAD) # Extract ticket number from branch (e.g., feature/JIRA-123-description) TICKET=$(echo "$BRANCH_NAME" | grep -oE '[A-Z]+-[0-9]+') if [ -n "$TICKET" ]; then # Prepend ticket number if not already present if ! grep -q "$TICKET" "$COMMIT_MSG_FILE"; then sed -i.bak "1s/^/[$TICKET] /" "$COMMIT_MSG_FILE" fi fi Pre-push Hook Run Tests Before Push 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash # .git/hooks/pre-push echo "Running tests before push..." # Run test suite npm test if [ $? -ne 0 ]; then echo "ERROR: Tests failed! Push aborted." exit 1 fi # Check coverage threshold npm run coverage COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct') if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "ERROR: Coverage below 80%! Current: $COVERAGE%" exit 1 fi echo "All checks passed. Pushing..." Prevent Force Push to Main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/bin/bash # .git/hooks/pre-push PROTECTED_BRANCHES="main master" CURRENT_BRANCH=$(git symbolic-ref --short HEAD) # Check if force pushing while read local_ref local_sha remote_ref remote_sha; do if echo "$PROTECTED_BRANCHES" | grep -qw "$CURRENT_BRANCH"; then if [ "$remote_sha" != "0000000000000000000000000000000000000000" ]; then # Check if this is a force push if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then echo "ERROR: Force push to $CURRENT_BRANCH is not allowed!" exit 1 fi fi fi done Post-merge Hook Auto-install Dependencies 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/bin/bash # .git/hooks/post-merge CHANGED_FILES=$(git diff-tree -r --name-only ORIG_HEAD HEAD) # Check if package.json changed if echo "$CHANGED_FILES" | grep -q "package.json"; then echo "package.json changed, running npm install..." npm install fi # Check if requirements.txt changed if echo "$CHANGED_FILES" | grep -q "requirements.txt"; then echo "requirements.txt changed, running pip install..." pip install -r requirements.txt fi # Check if migrations added if echo "$CHANGED_FILES" | grep -q "migrations/"; then echo "New migrations detected!" echo "Run: python manage.py migrate" fi Using pre-commit Framework The pre-commit framework manages hooks declaratively. ...

February 25, 2026 Â· 6 min Â· 1136 words Â· Rob Washington

Git Advanced Workflows: Beyond Push and Pull

You know git add, commit, push, and pull. That gets you through 90% of daily work. But the remaining 10%—untangling merge conflicts, finding bug introductions, managing multiple branches simultaneously—requires deeper knowledge. Interactive Rebase The most powerful tool for cleaning up history before sharing. 1 2 3 4 5 # Rebase last 5 commits git rebase -i HEAD~5 # Rebase onto main git rebase -i main The editor opens with commits listed oldest-first: ...

February 25, 2026 Â· 9 min Â· 1834 words Â· Rob Washington

Git Workflow Patterns: From Solo to Team

Git is flexible enough to support almost any workflow. That flexibility is both a blessing and a curse — without clear patterns, teams end up with merge conflicts, lost work, and frustration. Here are workflows that work. Solo Developer: Simple Trunk When you’re working alone, keep it simple: 1 2 3 4 5 6 7 8 # Work directly on main git checkout main git pull # Make changes git add . git commit -m "Add feature X" git push That’s it. No branches, no PRs, no ceremony. Save complexity for when you need it. ...

February 24, 2026 Â· 7 min Â· 1451 words Â· Rob Washington

Git Bisect: Finding the Commit That Broke Everything

Something’s broken. It worked last week. Somewhere in the 47 commits since then, someone introduced a bug. You could check each commit manually, or you could let git do the work. git bisect performs a binary search through your commit history to find the exact commit that introduced a problem. Instead of checking 47 commits, you check about 6. The Basic Workflow 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # Start bisecting git bisect start # Mark current commit as bad (has the bug) git bisect bad # Mark a known good commit (before the bug existed) git bisect good v1.2.0 # or git bisect good abc123 # Git checks out a commit halfway between good and bad # Test it, then tell git the result: git bisect good # This commit doesn't have the bug # or git bisect bad # This commit has the bug # Git narrows down and checks out another commit # Repeat until git finds the first bad commit # When done, return to original state git bisect reset Example Session 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 $ git bisect start $ git bisect bad # HEAD is broken $ git bisect good v2.0.0 # v2.0.0 worked fine Bisecting: 23 revisions left to test after this (roughly 5 steps) [abc123...] Add caching layer $ ./run-tests.sh Tests pass! $ git bisect good Bisecting: 11 revisions left to test after this (roughly 4 steps) [def456...] Refactor auth module $ ./run-tests.sh FAIL! $ git bisect bad Bisecting: 5 revisions left to test after this (roughly 3 steps) [ghi789...] Update dependencies # ... continue until: abc123def456789 is the first bad commit commit abc123def456789 Author: Someone <someone@example.com> Date: Mon Feb 20 14:30:00 2024 Fix edge case in login flow This commit introduced the bug! $ git bisect reset Automated Bisecting If you have a script that can test for the bug: ...

February 24, 2026 Â· 6 min Â· 1110 words Â· Rob Washington

Git Hooks: Automate Quality Before It Reaches the Repo

Git hooks are scripts that run at specific points in the Git workflow. They’re the first line of defense against bad commits — catching issues before they pollute your repository history. Set them up once, forget about them, and let automation enforce quality. Hook Types Client-side hooks (your machine): pre-commit — Before commit is created prepare-commit-msg — Before commit message editor opens commit-msg — After commit message is entered pre-push — Before push to remote Server-side hooks (repository server): ...

February 23, 2026 Â· 6 min Â· 1086 words Â· Rob Washington

Git Workflow Strategies: Branching Models That Scale

Git doesn’t enforce a workflow. You can branch however you want, merge whenever you want, push whatever you want. That freedom is powerful and dangerous. A good branching strategy reduces merge conflicts, enables parallel development, and makes releases predictable. A bad one creates confusion, broken builds, and deployment fear. Trunk-Based Development The simplest model: everyone commits to main, frequently. m a i n : ─ f ( ─ e h ─ a o ● │ t u ─ u r ─ r s ─ e ) ● ─ ─ f ( ─ i h ● │ x o ─ u ─ r ─ s ● ) ─ ─ ─ ● f ( ─ e h ─ a o ─ t u ● │ u r ─ r s ─ e ) ─ ● ─ ─ ─ Rules: ...

February 23, 2026 Â· 7 min Â· 1347 words Â· Rob Washington