Git, CI/CD & GitOpsXXV · HooksHooks
Hooks and policy enforcement — the limits of client-side control
What you'll learn
- Bypass a pre-commit hook with `git commit --no-verify` and a pre-push hook with `git push --no-verify`
- Recognise that any client-side hook can be bypassed by the engineer running it
- Design policies that assume client-side hooks are advisory: server-side enforcement, CI gates, code review
- Use pre-commit and pre-push as fast feedback, not as enforcement
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The previous lessons established what hooks are, where they live, and what each hook reads. This lesson is the corrective: every client-side hook can be bypassed by the engineer running it, and the production framing of hooks depends on accepting that fact. Server-side hooks and CI are the actual enforcement tiers; client-side hooks are fast feedback. The discipline is to design policies that survive a client-side bypass.
The bypass surface
Git provides a flag that skips client-side hooks on demand.
git commit --no-verify skips pre-commit and commit-msg;
git push --no-verify skips pre-push. The flag is a single
character (-n) and is documented in the man page as the
official escape hatch for the case where the hook is wrong
(misconfigured, blocking a legitimate commit, failing on an
environment issue).
# A pre-commit hook refuses to record the commit
git commit -m "fix typo"
# pre-commit: ERROR: staged content matches AWS access key pattern
# commit aborted
# The engineer bypasses the hook with --no-verify
git commit --no-verify -m "fix typo"
# [main 8a3f9d2] fix typo
The bypass is intentional and visible. The engineer knows they are bypassing the hook (the flag is named); the commit object is still recorded with the staged content. The hook’s stderr is still printed (so the engineer knows what they bypassed); the only thing skipped is the hook’s exit code check.
flowchart LR
C["git commit"] --> FLAG{"--no-verify?"}
FLAG -->|"no"| PC["pre-commit hook runs"]
FLAG -->|"yes"| SKIP["pre-commit and commit-msg skipped"]
PC --> CHECK{"hook exit 0?"}
CHECK -->|"yes"| CMT["commit recorded"]
CHECK -->|"no"| AB["commit aborted"]
SKIP --> CMT
The same surface exists for pre-push:
# A pre-push hook refuses to send the pack
git push origin feature/x
# pre-push: ERROR: direct push to main is forbidden
# push aborted
# The engineer bypasses the hook with --no-verify
git push --no-verify origin feature/x
# To git@github.com:acme/iac.git
# 8a3f9d2..a2c1b7f feature/x -> feature/x
The bypass surface also includes the case where the engineer
has not installed the hook at all. A fresh clone has only the
.sample files; if the engineer does not run the bootstrap
script, the hook is not active. The hook cannot enforce
anything for an engineer who has not installed it.
The three enforcement tiers
A policy that must hold for every engineer belongs in one of three enforcement tiers, in increasing order of strength:
flowchart LR
T1["Tier 1: client-side hooks\n(advisory, bypassable)"] --> T2["Tier 2: CI\n(runs on every push, mandatory)"]
T2 --> T3["Tier 3: server-side hooks\n(refuse the push, structural)"]
T3 --> T4["Tier 4: hosted policy API\n(branch protection, required status checks)"]
- Tier 1: client-side hooks. Fast feedback for the
engineer. Can be bypassed with
--no-verify; can be uninstalled entirely. Use for: format checks, lint, secret scanning (fast feedback that the engineer would want even if not enforced). - Tier 2: CI pipeline. Runs on every push (or every PR). Cannot be bypassed by the engineer - if the CI check fails, the merge is blocked (assuming branch protection requires the check). Use for: full test suite, dependency audit, plan generation, slow lint, format verification.
- Tier 3: server-side hooks (self-hosted). Runs on the receiving bare repository. Cannot be bypassed from the client. Use for: branch protection, signing requirements, commit-message policy.
- Tier 4: hosted policy API (GitHub, GitLab, etc.). The hosted equivalent of server-side hooks. Use for: branch protection, required status checks, required reviews, signed commits, signed tags.
The relationship between the tiers is layered, not redundant.
Tier 1 catches the issue at the engineer’s machine, before the
push. Tier 2 catches the issue on the server, after the push
but before the merge. Tier 3/4 catches the issue at the
server’s pre-receive, refusing the push entirely. Each tier
is stronger than the previous; the production discipline is to
make every important policy appear in at least Tier 2, with
the bypassable Tier 1 as a fast-feedback convenience.
Why CI is the actual enforcement tier
In a hosted environment, CI is the enforcement tier that catches everything client-side hooks miss. A pre-commit hook that is bypassed records the commit; a CI check that fails on the same content blocks the merge (assuming branch protection requires the check). The CI check sees the same content the hook would have seen, runs the same checks (or stronger), and its verdict is structural - the merge cannot proceed without it.
# CI runs the same secret scan as the pre-commit hook
# .github/workflows/secret-scan.yml
name: secret-scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: $GITHUB_TOKEN
The CI check is mandatory: branch protection requires the
secret-scan check to pass before the PR can merge. The
pre-commit hook is the same check, run earlier, with faster
feedback. The two are complementary: pre-commit gives the
engineer a 200ms hint; CI gives the team a structural gate.
Designing policies that survive client-side bypass
A policy that is robust to client-side bypass has three properties:
- It runs in Tier 2 or higher. The policy is checked by CI on every push (or in a server-side hook). The engineer cannot push a violation that bypasses the check.
- It is required by branch protection. The check is in the list of required status checks for the protected branch. The merge is blocked until the check passes.
- It is auditable. The check’s verdict is recorded in the CI logs (and in the Events API for hosted platforms). A bypass attempt is visible in the audit trail.
A policy that depends only on Tier 1 (client-side hooks) has
none of these properties. The engineer can bypass with
--no-verify; the engineer can uninstall the hook; the bypass
leaves no audit trail. The policy is a suggestion.
Production discipline
- Assume every client-side hook is bypassable. Design
every policy to survive
git commit --no-verifyandgit push --no-verify. - Put the policy in CI, not in a hook. A CI check is mandatory (assuming branch protection requires it); a hook is advisory. The check belongs in CI.
- Require the CI check in branch protection. A check that runs but is not required is a check the engineer can ignore. The check must be in the required-status-checks list.
- Treat the client-side hook as fast feedback. A pre-commit hook that catches a secret in 200ms is a courtesy to the engineer; the actual enforcement is the CI check that catches the same secret on the server.
- Audit bypasses. A
--no-verifybypass on a commit that should not have bypassed is a signal that the policy needs to move to a stronger tier, not a signal that the engineer did something wrong.
Cross-course references
- Git, CI/CD & GitOps - Part XXII (ForcePush) lesson 05
covers branch protection, which is the structural
enforcement that complements client-side
--force-with-lease. - Git, CI/CD & GitOps - Part XXIV (Bisect) lesson 06 covers the recovery from a bad bisect run, which is relevant when a bypass introduced a bug that needs to be located.
- CI/CD Pipeline Patterns - Parts II (PipelineStages) and VIII (BranchPolicies) cover the CI and hosted enforcement tiers that complement client-side hooks.
Quiz
Knowledge check · 4 questions
Q1. An engineer runs `git commit --no-verify -m 'wip'` to bypass a pre-commit secret-scan hook. Where should the policy that no AWS key may be committed live, so that the bypass does not allow the leak to reach the remote?
Q2. `git commit --no-verify` skips both the `pre-commit` and `commit-msg` hooks, while `git push --no-verify` (equivalent to `git push -n`) skips the `pre-push` hook, leaving the engineer fully able to bypass any client-side enforcement.
Q3. List the four enforcement tiers for a Git policy, in order of increasing strength, and name the mechanism each tier uses.
Q4. Diagnose why a team that relies on a client-side pre-commit hook for policy compliance has inconsistent enforcement, and recommend the layered enforcement that would make the policy robust.
A team mandates that no commit may contain an AWS access key. The team installs a pre-commit hook that scans staged content and refuses to record the commit if a match is found. After a month, an audit reveals three commits in `main` that contain AWS keys. The team asks why the pre-commit hook did not catch them.
Passing score: 75%. Answers are checked in this browser.