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 init

After 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.

.husky/pre-push
#!/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 dg

How 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 1

The 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

CodeMeaningGit behavior
0No breaking changes found. API contract is intact.Push proceeds normally.
1Breaking changes detected. Strict mode engaged.Push is blocked. Nothing is sent to the remote.
2Infrastructure 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-verify

This 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/hotfix

Notice 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/payments

This 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.

.husky/pre-merge-commit
#!/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 dg

What 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 strategypre-merge-commitpost-mergeNotes
git merge feature (creates commit)FiresFiresFull protection. Both hooks run.
git merge --ff feature (fast-forward)Does not fireFiresOnly post-merge provides advisory output.
git merge --no-ff feature (force commit)FiresFiresSame as regular merge. Full protection.
git merge --squash featureDoes not fireDoes not fireNeither 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 false

Post-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.

.husky/post-merge
#!/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 || true

The || 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:

  1. VSCODE_GIT_ASKPASS_MAIN is set — VS Code sets this environment variable in its child processes to handle authentication prompts.
  2. ! -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.json file 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

  1. Developer makes changes and commits via Source Control panel.
  2. Developer clicks "Sync Changes" (or the cloud upload icon).
  3. VS Code runs git push internally.
  4. Pre-push hook detects the VS Code environment and runs in advisory mode.
  5. Push completes successfully. A .dg-report.json file appears in the project root.
  6. 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

.dg-report.json
{
  "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:

  1. 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.
  2. 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.
LayerTriggerModeBlocking
Pre-push hook (terminal)git pushStrictYes (exit 1)
Pre-push hook (VS Code)Sync ChangesAdvisoryNo (report file)
Pre-merge-commit hookgit mergeStrictYes (exit 1)
Post-merge hookAfter merge completesAdvisoryNo (report file)
GitHub Actions CIPR opened/updatedAdvisoryNo (PR comment)

For full CI/CD configuration details, see the CI/CD Integration page.


Environment variables

VariableSet byPurpose
DG_HOOKHook scriptTells the CLI which hook is calling it. Values: pre-push, pre-merge-commit, post-merge.
NODE_OPTIONSHook scriptMemory limit for the Node.js process. Default: 512 MB.
VSCODE_GIT_ASKPASS_MAINVS CodeAutomatically set when running inside VS Code. Used to detect advisory mode.