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:
- Checks GCE (scandoraproject) for opnsense-dev and
dumbo-dev via
gcloud - Checks GCE (coop-389306) for bogart-dev via
gcloud --project=coop-389306 - Checks AWS (us-west-2) for pluto-dev and mickey via
aws ec2 describe-instances - Reports running instances with their cost rate and teardown command
- 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:
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¶
- Always commit after dev work -- triggers the hook
- Tear down immediately after validation -- don't wait
- Trust the hook -- if you see it, run the teardown
- 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-- validfix/61-op-inject-bugs-- validworktree-fix+231-restart-crash-- valid (prefix stripped,+accepted)feat/pretooluse-enforcement-- blocked (no issue number)
Allowed without issue number:
mainbranch (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>.
Related Documentation¶
scripts/opnsense-dev/dev-up.sh/dev-down.sh-- opnsense-dev lifecyclecloud/ansible/scripts/dev-up-*.sh/dev-down-*.sh-- other dev instance lifecyclesdocs/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