Git Hooks
Diff Guardian integrates with Husky to provide three git hooks: pre-push (strict gatekeeper), pre-merge-commit, and post-merge. Together they enforce API contracts at every critical git operation, preventing breaking changes from reaching shared branches.
Setup
Diff Guardian hooks are configured through Husky. If you already have Husky set up, add the hook scripts to your .husky/ directory. If not, install Husky first:
# Install Husky (if not already installed)
npm install --save-dev husky
npx husky initAfter initializing Husky, create each hook file described below inside the.husky/ directory. Husky will automatically invoke these scripts at the appropriate git lifecycle events.
Pre-push hook
The primary enforcement point. This hook runs the full Diff Guardian pipeline before every git push. If breaking changes are detected, the push is blocked with exit code 1 and no data leaves your local machine.
#!/bin/sh
# Node version guard
NODE_MAJOR=$(node -e "process.stdout.write(process.versions.node.split('.')[0])" 2>/dev/null)
if [ -z "$NODE_MAJOR" ] || [ "$NODE_MAJOR" -lt 18 ]; then
echo " [diff-guardian] Skipping: requires Node.js 18 or higher."
exit 0
fi
# VS Code Source Control UI — advisory mode
if [ -n "$VSCODE_GIT_ASKPASS_MAIN" ] && [ ! -t 1 ]; then
NODE_OPTIONS="--max-old-space-size=512" DG_HOOK=pre-push npx dg --report-file .dg-report.json || true
exit 0
fi
# Terminal — strict gatekeeper
echo " Diff-Guardian: Running pre-push API contract gatekeeper..."
NODE_OPTIONS="--max-old-space-size=512" DG_HOOK=pre-push npx dgHow push blocking works
When you run git push in the terminal, Git invokes the pre-push hook before transmitting any objects to the remote. The hook runs npx dg, which executes the full 4-phase pipeline (diff, parse, classify, trace). If any breaking change is detected, the CLI exits with code 1.
Git interprets a non-zero exit code from a hook as "abort the operation." The push is cancelled immediately. Nothing is sent to the remote. This is the exact same mechanism Git uses for pre-commit hooks — it is standard Git behavior, not a Diff Guardian workaround.
What the developer sees
When a push is blocked, the terminal output looks like this:
$ git push origin feature/payments
Diff-Guardian: Running pre-push API contract gatekeeper...
Diff-Guardian API Analysis
Base: main -> Head: feature/payments
[BREAKING] Changes (2)
> processPayment (signature_change)
src/api/payments.ts:42
R01: Parameter 'currency' was removed.
Affected call sites (3):
X src/checkout/handler.ts:18 -- provides 3 arg(s), needs 2
OK src/invoices/gen.ts:31 -- Fixed by developer in this PR
> UserConfig (interface_property_removed)
src/types/config.ts:8
R26: Property 'timeout' was removed from interface.
────────────────────────────────────────
[STRICT MODE]
2 breaking changes found. Exiting with code 1.
error: failed to push some refs to 'origin'
hint: the pre-push hook returned exit code 1The developer must fix the breaking changes (or add the removed parameter back, update the interface, etc.) and commit again before the push will succeed.
Exit codes
| Code | Meaning | Git behavior |
|---|---|---|
0 | No breaking changes found. API contract is intact. | Push proceeds normally. |
1 | Breaking changes detected. Strict mode engaged. | Push is blocked. Nothing is sent to the remote. |
2 | Infrastructure error (missing grammar, OOM). | Hook treats this as a failure and blocks the push to be safe. |
Bypassing hooks with --no-verify
Git provides a built-in escape hatch: the --no-verify flag (also -n). When you pass this flag, Git skips allclient-side hooks for that operation — including the Diff Guardian hook.
Bypassing pre-push
git push --no-verifyThis sends your commits to the remote without running the pre-push hook. The push will succeed regardless of whether breaking changes exist.
When to use it
- Hotfixes — You need to ship a critical fix immediately and will address the API contract issue in a follow-up.
- Documentation-only changes — You know your commit only touches markdown or non-code files, and you want to skip the analysis time.
- Intentional breaking changes — You have already coordinated the breaking change with your team and want to push it through.
What the developer sees
$ git push --no-verify origin feature/hotfix
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 312 bytes | 312.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To github.com:your-org/your-repo.git
abc1234..def5678 feature/hotfix -> feature/hotfixNotice that the Diff Guardian analysis line is completely absent. Git did not invoke the hook at all.
Bypassing pre-merge-commit
git merge --no-verify feature/paymentsThis merges the branch without running the pre-merge-commit hook. The merge commit is created immediately without API analysis. The post-merge hook will still fire after the merge completes.
What the developer sees
$ git merge --no-verify feature/payments
Merge made by the 'ort' strategy.
src/api/payments.ts | 12 ++++++------
src/types/config.ts | 3 +--
2 files changed, 7 insertions(+), 8 deletions(-)
# Note: post-merge hook still runs (advisory only)
Diff-Guardian: Generating post-merge API report...Pre-merge-commit hook
Runs before a merge commit is created. Catches breaking changes at the point of merging a feature branch into main. Like the pre-push hook, it blocks the operation with exit code 1 if breaking changes are found.
#!/bin/sh
NODE_MAJOR=$(node -e "process.stdout.write(process.versions.node.split('.')[0])" 2>/dev/null)
if [ -z "$NODE_MAJOR" ] || [ "$NODE_MAJOR" -lt 18 ]; then
echo " [diff-guardian] Skipping: requires Node.js 18+."
exit 0
fi
echo " Diff-Guardian: Running pre-merge API audit..."
NODE_OPTIONS="--max-old-space-size=512" DG_HOOK=pre-merge-commit npx dgWhat a blocked merge looks like
$ git merge feature/payments
Diff-Guardian: Running pre-merge API audit...
Diff-Guardian API Analysis
Base: main -> Head: feature/payments
[BREAKING] Changes (1)
> processPayment (signature_change)
src/api/payments.ts:42
R01: Parameter 'currency' was removed.
────────────────────────────────────────
[STRICT MODE]
1 breaking change found. Exiting with code 1.
Automatic merge failed; fix conflicts and then commit the result.Fast-forward merges
A fast-forward merge moves the branch pointer forward without creating a merge commit. Since there is no merge commit, the pre-merge-commithook does not fire.
This is standard Git behavior. A fast-forward merge is essentially the same as moving a label — no new commit is created, so no commit hook runs.
| Merge strategy | pre-merge-commit | post-merge | Notes |
|---|---|---|---|
git merge feature (creates commit) | Fires | Fires | Full protection. Both hooks run. |
git merge --ff feature (fast-forward) | Does not fire | Fires | Only post-merge provides advisory output. |
git merge --no-ff feature (force commit) | Fires | Fires | Same as regular merge. Full protection. |
git merge --squash feature | Does not fire | Does not fire | Neither hook fires. Use dg check --staged instead. |
If you want to ensure the pre-merge-commit hook always runs, configure your workflow to use --no-ff merges:
# Force merge commits (recommended for protected branches)
git config merge.ff falsePost-merge hook
Runs after a merge completes (both fast-forward and regular merges). Generates a report of API changes that were just merged, without blocking. This hook is purely informational — it creates an audit trail.
#!/bin/sh
NODE_MAJOR=$(node -e "process.stdout.write(process.versions.node.split('.')[0])" 2>/dev/null)
if [ -z "$NODE_MAJOR" ] || [ "$NODE_MAJOR" -lt 18 ]; then
exit 0
fi
echo " Diff-Guardian: Generating post-merge API report..."
NODE_OPTIONS="--max-old-space-size=512" DG_HOOK=post-merge npx dg --report-file .dg-report.json || trueThe || trueat the end ensures the hook never fails. Post-merge is advisory — it should never interrupt the developer's workflow. The report is written to .dg-report.json for later review.
VS Code Source Control (Sync Changes)
When you click "Sync Changes" in VS Code's Source Control panel (or use the sync button in the status bar), VS Code performs agit pull followed by a git push behind the scenes. This push triggers the pre-push hook, but with a critical difference: it runs in a non-interactive environment.
How Diff Guardian detects VS Code
The hook script checks two conditions to determine if it is running inside VS Code's Source Control panel:
VSCODE_GIT_ASKPASS_MAINis set — VS Code sets this environment variable in its child processes to handle authentication prompts.! -t 1— Standard output is not a TTY (terminal). This distinguishes the Source Control panel from VS Code's integrated terminal.
When both conditions are true, the hook switches to advisory mode:
- The push always goes through (the hook exits 0 regardless of results).
- A
.dg-report.jsonfile is written to the project root with the full analysis. - If you have the integrated terminal open, a push from it uses the strict gatekeeper path instead.
Complete VS Code workflow
- Developer makes changes and commits via Source Control panel.
- Developer clicks "Sync Changes" (or the cloud upload icon).
- VS Code runs
git pushinternally. - Pre-push hook detects the VS Code environment and runs in advisory mode.
- Push completes successfully. A
.dg-report.jsonfile appears in the project root. - Developer opens the report file in VS Code to review any flagged changes. The file is JSON with a structured format showing breaking changes, warnings, and safe changes.
Report file format
{
"timestamp": "2026-04-17T10:30:00Z",
"hook": "pre-push",
"base": "main",
"head": "feature/payments",
"summary": {
"total": 5,
"breaking": 2,
"warning": 1,
"safe": 2
},
"changes": [
{
"symbol": "processPayment",
"file": "src/api/payments.ts",
"line": 42,
"rule": "R01",
"severity": "breaking",
"message": "Parameter 'currency' was removed."
}
]
}Add .dg-report.json to your .gitignore to prevent it from being committed.
Full lifecycle: from local push to PR comment
When git hooks and CI/CD are both configured, a single push triggers two layers of protection:
- Local (pre-push hook) — Runs instantly on your machine. If breaking changes are found, the push is blocked before any code reaches the remote. This is the first line of defense.
- Remote (GitHub Actions)— If the push succeeds (no breaking changes locally), the CI workflow runs on GitHub's servers. It performs the same analysis but posts the results as a PR comment. CI mode is advisory — it exits 0 and never blocks the merge directly.
| Layer | Trigger | Mode | Blocking |
|---|---|---|---|
| Pre-push hook (terminal) | git push | Strict | Yes (exit 1) |
| Pre-push hook (VS Code) | Sync Changes | Advisory | No (report file) |
| Pre-merge-commit hook | git merge | Strict | Yes (exit 1) |
| Post-merge hook | After merge completes | Advisory | No (report file) |
| GitHub Actions CI | PR opened/updated | Advisory | No (PR comment) |
For full CI/CD configuration details, see the CI/CD Integration page.
Environment variables
| Variable | Set by | Purpose |
|---|---|---|
DG_HOOK | Hook script | Tells the CLI which hook is calling it. Values: pre-push, pre-merge-commit, post-merge. |
NODE_OPTIONS | Hook script | Memory limit for the Node.js process. Default: 512 MB. |
VSCODE_GIT_ASKPASS_MAIN | VS Code | Automatically set when running inside VS Code. Used to detect advisory mode. |
Related
- CI/CD Integration — GitHub Actions workflow
- dg check — manual pre-commit checks
- Configuration — tuning hook behavior