Skip to content

Git Hooks

Overview

Git hooks enforce project policy and prevent costly mistakes automatically. All hooks run on every developer's machine -- no CI dependency.

Active Hooks

Hook Location Managed by Purpose
pre-commit .git/hooks/ pre-commit Linters, secrets
commit-msg .git/hooks/ pre-commit Conventional commit
pre-push .git/hooks/ pre-commit Block pushes to main
post-commit .git/hooks/ Manual Dev instance cost warning

Setup (required after every fresh clone)

# Install the pre-commit framework (if not already installed)
brew install pre-commit   # macOS

# Install all hooks (pre-commit, commit-msg, pre-push)
pre-commit install --hook-type pre-commit
pre-commit install --hook-type commit-msg
pre-commit install --hook-type pre-push

# post-commit is already in .git/hooks/ -- no install needed

Important: Do not set git config core.hooksPath. The pre-commit framework manages hooks in .git/hooks/ directly. Setting core.hooksPath causes pre-commit to refuse installation and silently disables all linters.

Pre-Commit Linters (.pre-commit-config.yaml)

Runs on every git commit. Tiers defined in .pre-commit-config.yaml:

Tier Tools What it catches
1 gitleaks, trufflehog Secrets, API keys, tokens
2 trailing-ws, eof-fixer, check-yaml, shellcheck File hygiene
3 ansible-lint Ansible role/playbook issues
4 terraform_fmt, terraform_validate, tfsec Terraform formatting/security
5 hadolint Dockerfile issues
6 shellcheck Shell script bugs and style
7 yamllint YAML formatting
8 black Python formatting
9 mkdocs-validate Broken doc links
10 conventional-pre-commit Commit message format
11 worktree-protect, no-push-to-main Policy enforcement

Run manually against all files: pre-commit run --all-files

Pre-Push Guard (scripts/hooks/no-push-to-main.sh)

Blocks any direct push to main or master. Enforces GitHub Flow: all changes must go through a branch and PR.

BLOCKED: Direct push to 'main' is not allowed.

  This repo uses GitHub Flow. To land changes on main:
  1. Ensure your branch is pushed:  git push -u origin <branch>
  2. Create a PR:                   gh pr create
  3. Merge via GitHub UI after review

Emergency bypass (use sparingly): SKIP=no-push-to-main git push origin main

Post-Commit Hook: Ephemeral Instance Cost Warning

Location: .git/hooks/post-commit (gitignored -- not managed by pre-commit)

Purpose: After every commit, check for running ephemeral dev instances and display a reminder with teardown commands.

Monitored Instances

Instance Provider Cost Teardown
opnsense-dev GCE scandoraproject ~$0.14/hr dev-down.sh
dumbo-dev GCE scandoraproject ~$0.07/hr dev-down-dumbo.sh
bogart-dev GCE coop-389306 ~$0.07/hr dev-down-bogart.sh
pluto-dev AWS us-west-2 ~$0.02/hr dev-down-pluto.sh
mickey AWS us-west-2 ~$0.04/hr aws ec2 stop-instances

How It Works

After each commit, the hook:

  1. Checks GCE (scandoraproject) for opnsense-dev and dumbo-dev via gcloud
  2. Checks GCE (coop-389306) for bogart-dev via gcloud --project=coop-389306
  3. Checks AWS (us-west-2) for pluto-dev and mickey via aws ec2 describe-instances
  4. Reports running instances with their cost rate and teardown command
  5. Exits silently if no instances are running or CLI tools are unavailable

Checks are skipped gracefully if gcloud or aws are not in $PATH or credentials are unavailable.

Example Output

$ git commit -m "fix(opnsense): delegate ZeroTier Central API"
[main 7120165] fix(opnsense): delegate ZeroTier Central API
 1 file changed, 8 insertions(+)

═══════════════════════════════════════════════
  Ephemeral instances running
═══════════════════════════════════════════════

  opnsense-dev (~$0.14/hr)
    Tear down: scripts/opnsense-dev/dev-down.sh

  pluto-dev (~$0.02/hr)
    Tear down: cloud/ansible/scripts/dev-down-pluto.sh

Tear down when done to avoid unnecessary costs.

When no instances are running, the hook exits silently with no output.

Post-Commit Installation

The post-commit hook lives only in .git/hooks/post-commit (gitignored, not managed by pre-commit). It is preserved across pre-commit install runs since pre-commit only touches its own hook types.

To test:

.git/hooks/post-commit && echo "OK (no instances running)"

Cost Savings

Without hook:

  • Forget to tear down after commit
  • Instance runs overnight (8 hours): $0.14/hr x 8 = $1.12 (opnsense-dev alone)
  • All 5 instances running overnight: ~$2.72

With hook:

  • Reminder on every commit while instances are running
  • Backup: Prometheus alerts fire at 1hr (warning) and 4hr (critical) via DevVMRunningTooLong

Best Practices

  1. Always commit after dev work -- triggers the hook
  2. Tear down immediately after validation -- don't wait
  3. Trust the hook -- if you see it, run the teardown
  4. Prometheus is the backstop -- DevVMRunningTooLong at 1hr is multi-layered safety

Maintenance

Updating the Post-Commit Hook

The post-commit hook lives only in .git/hooks/post-commit (gitignored). To update:

# Edit the hook directly
vim .git/hooks/post-commit

# Test it (should exit silently when nothing is running)
.git/hooks/post-commit && echo "OK"

Debugging

# Check what's running
gcloud compute instances list \
  --project=scandoraproject \
  --filter="name=(opnsense-dev OR dumbo-dev)"

gcloud compute instances list \
  --project=coop-389306 \
  --filter="name=bogart-dev"

aws ec2 describe-instances \
  --region us-west-2 \
  --filters \
    "Name=tag:Name,Values=pluto-dev,mickey" \
    "Name=instance-state-name,Values=running"

# Manual teardown
./scripts/opnsense-dev/dev-down.sh
./cloud/ansible/scripts/dev-down-dumbo.sh
./cloud/ansible/scripts/dev-down-bogart.sh
./cloud/ansible/scripts/dev-down-pluto.sh

Disabling Temporarily

# Rename to disable
mv .git/hooks/post-commit .git/hooks/post-commit.disabled

# Restore later
mv .git/hooks/post-commit.disabled .git/hooks/post-commit

Integration with Development Workflow

Typical Development Session

# 1. Provision dev instance(s)
./cloud/ansible/scripts/dev-up-dumbo.sh

# 2. Do development work, make commits
git commit -m "fix: improve monitoring config"
# Hook shows reminder if any instances are running

# 3. When validation is complete, tear down
./cloud/ansible/scripts/dev-down-dumbo.sh

# 4. Next commit shows no warnings
git commit -m "feat: validate dumbo-dev monitoring stack"
# (no hook output -- nothing running)

Claude Code PreToolUse Hooks (.claude/settings.json)

Claude Code hooks fire before tool execution, enforcing policy without CI dependency. Configured in .claude/settings.json under hooks.PreToolUse.

Hook Matcher Script
Issue enforcement Write, Edit enforce-issue-branch.sh
Shared file protection Write, Edit protect-shared-files.sh
Long-command advisory Bash enforce-tee.sh

Issue Enforcement Hook

Denies Edit/Write operations on branches that don't embed a GitHub issue number in the branch name. Enforces the issue-first workflow: create an issue, then /start-issue <number> to begin work.

Branch name format: <type>/<issue-number>-<description>

The hook also accepts + as a separator (e.g., fix+231-restart-crash) because EnterWorktree replaces / with + in worktree directory and branch names.

Examples:

  • feat/227-pretooluse-enforcement -- valid
  • fix/61-op-inject-bugs -- valid
  • worktree-fix+231-restart-crash -- valid (prefix stripped, + accepted)
  • feat/pretooluse-enforcement -- blocked (no issue number)

Allowed without issue number:

  • main branch (config/memory edits)
  • Detached HEAD (edge case)

Error message:

BLOCKED (issue enforcement): Branch '<name>' has no issue number.

  Branch names must follow: <type>/<issue-number>-<description>
  Example: feat/227-pretooluse-enforcement

  Create a GitHub issue first, then use /start-issue <number>.
  • scripts/opnsense-dev/dev-up.sh / dev-down.sh -- opnsense-dev lifecycle
  • cloud/ansible/scripts/dev-up-*.sh / dev-down-*.sh -- other dev instance lifecycles
  • docs/operations/monitoring.md -- cost control alerts (DevVMRunningTooLong)

Last updated: 2026-03-28 Status: Active and tested Instances monitored: opnsense-dev, dumbo-dev, bogart-dev, pluto-dev, mickey