Pre-Push Gate With 30-Min TTL | Claude Code Hook

A pre-push validation gate with a 30-minute TTL runs parallel reviewers on changed files before git push. Zero broken pushes in 2+ weeks of daily use.

Share

Pre-Push Gate With a 30-Minute TTL: How I Stopped Pushing Broken Code | AI PM Portfolio

Pre-Push Gate With a 30-Minute TTL: How I Stopped Pushing Broken Code

April 11, 2026 · 8 min read · Claude Code / Developer Workflow

Last Updated: 2026-04-11

A pre-push validation gate works by decoupling review from the push itself. A Claude Code skill (/pre-push) runs parallel reviewers against only the changed files, then writes a flag file with a 30-minute TTL. The git pre-push hook checks that flag file -- if it exists and is under 30 minutes old, push proceeds. If not, push is blocked. This pattern eliminated broken pushes to staging and production entirely over two weeks of daily use.

Why does pushing broken code keep happening?

Every developer has pushed broken code. TypeScript errors that compiled locally but failed in CI. New files imported but never committed. Tests that passed on a stale branch. The cost is not the broken build itself -- it is the 15-45 minutes of context-switching to diagnose, fix, re-push, and wait for CI again.

In a production codebase with 206 modules, 189 routes, and 65+ database tables, the blast radius of a bad push is significant. A single broken import can cascade into Vercel build failures that block the entire team for hours. I tracked broken pushes over a month before building this gate: 7 out of 34 pushes (20.6%) triggered CI failures that required a follow-up commit.

The conventional answer is "just run tsc in a pre-push hook." I tried that. It nearly broke my workflow. My Claude Code operating system post touches on why hooks need to be fast or they get bypassed.

Why doesn't running tsc in a git pre-push hook work?

On a large Next.js + TypeScript codebase (640MB virtual memory footprint), running npx tsc --noEmit in a pre-push hook takes 15+ minutes. On a machine running Claude Code sessions, an IDE, and a browser simultaneously, it often hangs entirely -- stuck at 0% CPU while the OS swaps memory.

There are two deeper problems beyond raw performance:

  1. Stale directory drift. If the hook runs against a separate copy of the repo (a common pattern to avoid blocking the working directory), that copy drifts. Merge artifacts, deleted files, and missing dependencies accumulate. The hook reports false type errors that have nothing to do with your actual changes.
  2. Wrong scope. tsc --noEmit type-checks the entire project, not just the files you changed. On a 206-module codebase, you are paying the full compilation cost for a 3-file change. Worse, tsc cannot catch untracked files -- files that exist on disk but were never git add-ed. A new component file referenced by committed code passes local tsc but fails on Vercel because Vercel only sees what is in git.

According to the TypeScript documentation on project references, incremental compilation with "incremental": true helps, but the first cold run is still slow. And it does nothing about the untracked file problem.

How does the pre-push gate with a 30-minute TTL work?

The system has two parts: a Claude Code skill (/pre-push) that performs the actual review, and a git hook that checks whether the review happened recently enough to trust.

Part 1: The /pre-push skill

When you run /pre-push in Claude Code, the skill does the following:

  1. Checks git diff to identify which files changed relative to the target branch.
  2. Checks git ls-files --others --exclude-standard to find untracked files that might be imported by committed code.
  3. Maps changed files to reviewer domains -- backend (API routes, server logic), frontend (components, hooks), security (auth, RLS policies), and database (migrations, queries).
  4. Runs relevant reviewers in parallel. A 3-file frontend change triggers only the frontend reviewer. A migration + API route change triggers database + backend + security reviewers simultaneously.
  5. Reports findings and, if no critical issues are found, writes a flag file at /tmp/pre-push-gate.flag with the current timestamp.

The skill typically completes in 30-90 seconds because it only reviews changed files, not the entire codebase. Compare that to 15+ minutes for a full tsc run. The 4-reviewer pattern is documented in detail in my earlier post on parallel code review.

Part 2: The git pre-push hook

The hook is a short bash script in .githooks/pre-push:

#!/bin/bash
# Pre-push gate: requires /pre-push skill to have run within 30 minutes

FLAG_FILE="/tmp/pre-push-gate.flag"
MAX_AGE_MINUTES=30

# Check if flag file exists
if [ ! -f "$FLAG_FILE" ]; then
    echo ""
    echo "PUSH BLOCKED: No pre-push review found."
    echo "Run /pre-push in Claude Code first, then push again."
    echo ""
    exit 1
fi

# Check flag file age (in seconds)
if [ "$(uname)" = "Darwin" ]; then
    FILE_AGE=$(( $(date +%s) - $(stat -f %m "$FLAG_FILE") ))
else
    FILE_AGE=$(( $(date +%s) - $(stat -c %Y "$FLAG_FILE") ))
fi

MAX_AGE_SECONDS=$((MAX_AGE_MINUTES * 60))

if [ "$FILE_AGE" -gt "$MAX_AGE_SECONDS" ]; then
    echo ""
    echo "PUSH BLOCKED: Pre-push review is stale ($(( FILE_AGE / 60 )) min old)."
    echo "TTL is ${MAX_AGE_MINUTES} minutes. Run /pre-push again."
    echo ""
    exit 1
fi

echo "Pre-push gate passed (review is $(( FILE_AGE / 60 )) min old)."
exit 0

Output when blocked:

$ git push origin staging

PUSH BLOCKED: No pre-push review found.
Run /pre-push in Claude Code first, then push again.

Output when passing:

$ git push origin staging
Pre-push gate passed (review is 4 min old).
Enumerating objects: 12, done.
...

Why is the TTL 30 minutes?

The TTL is a deliberate design choice, not an arbitrary number. I tested three durations:

  • 10 minutes: Too short. After running /pre-push, making a small fix based on its feedback, and staging the change, 10 minutes had often elapsed. The gate blocked pushes that were effectively already reviewed.
  • 30 minutes: The sweet spot. Enough time to review findings, make 1-2 small fixes, commit, and push. Short enough that a review from the morning does not let you push unreviewed afternoon code.
  • 60 minutes: Too long. A developer can make substantial, unreviewed changes in an hour. The gate loses its protective value.

The 30-minute window matches the natural rhythm of a code-review-fix-push cycle. In 14 days of tracking, I never had a legitimate push blocked by the 30-minute TTL after making only small fixes.

How does this compare to other code quality gates?

Approach When it runs What it checks Feedback speed Catches untracked files? Best for
Pre-commit hook Every commit Lint, format, staged files 1-10 sec No Style enforcement
tsc in pre-push hook Every push Full type-check 5-20 min No Small codebases only
CI-only (GitHub Actions) After push Full test suite + types 3-10 min Yes (build fails) Comprehensive, but too late
Manual review Ad hoc Whatever you remember Varies Sometimes Nothing (error-prone)
Pre-push gate + TTL On demand, cached 30 min Changed files only, multi-domain 30-90 sec Yes Large codebases with AI tooling

The key insight: CI is comprehensive but reactive. You find out your push is broken 5-10 minutes after you have already context-switched to the next task. The pre-push gate is proactive -- it catches problems before they enter the remote branch. My post on CI consolidation covers why even well-structured CI pipelines cannot replace pre-push validation.

What does the /pre-push skill actually review?

The skill maps changed files to four reviewer domains and runs only the relevant ones:

Changed file pattern Reviewer triggered What it checks
app/api/**, lib/, server/ Backend Error handling, await patterns, timeout guards, response shapes
app/(routes)/**, components/ Frontend Hook rules, SSR safety, accessibility, Tailwind patterns
**/auth/**, **/rls/**, middleware.* Security Auth bypass, RLS gaps, IDOR, secrets in code
supabase/migrations/**, **/*.sql Database Migration safety, index coverage, FK constraints, RLS policies

A typical push touching 3-5 files triggers 1-2 reviewers and completes in under 60 seconds. The parallel execution means adding more reviewers does not proportionally increase review time -- four reviewers running simultaneously finish in roughly the same wall-clock time as one.

The untracked file check is critical. In one incident before this gate existed, another Claude Code session created component files locally, imported them from committed files, and never staged them. Local tsc passed because the files existed on disk. The push succeeded. Vercel failed with "Module not found" errors. The production deploy was blocked for 24+ hours. The /pre-push skill catches this by running git ls-files --others --exclude-standard and cross-referencing against imports in changed files.

What were the results after adopting this pattern?

I tracked push outcomes for 14 days before and 14 days after adopting the pre-push gate:

  • Before: 34 pushes, 7 CI failures (20.6% failure rate), estimated 5.25 hours lost to fix-and-repush cycles (45 min average per failure).
  • After: 41 pushes, 0 CI failures caused by preventable issues (0% failure rate). Two CI failures from infrastructure issues (Vercel timeout, npm registry outage) -- neither would have been caught by any local gate.
  • Average review time: 47 seconds per /pre-push invocation, compared to 15+ minutes for the previous tsc hook.
  • Net time saved: approximately 4.5 hours over two weeks, accounting for the 32 minutes total spent running /pre-push across 41 pushes.

The 30-minute TTL was triggered (push blocked due to stale review) 6 times. In every case, I had made changes after the review that warranted re-review. The gate worked as designed -- it did not block legitimate pushes, and it caught stale reviews that would have missed new changes.

How do you set this up in your own project?

Three steps:

Create the skill that writes the flag file. In Claude Code, this is a .claude/skills/ markdown file that defines the review steps and ends by writing the flag:

# At the end of the /pre-push skill, if all reviewers pass:
touch /tmp/pre-push-gate.flag

Add the pre-push hook (the bash script shown above) to .githooks/pre-push and make it executable:

chmod +x .githooks/pre-push

Configure the git hooks directory in your repo:

# Point git to a committed hooks directory
git config core.hooksPath .githooks

# Create the directory
mkdir -p .githooks

You can adapt the reviewer logic to any tool -- it does not have to be Claude Code. A script that runs eslint on changed files, checks for untracked imports, and runs targeted tsc on only the affected project references would achieve a similar result. The key innovation is the TTL-based flag file that decouples "review happened" from "push happening."

Frequently Asked Questions

Can someone bypass the pre-push gate with --no-verify?

Yes, git push --no-verify skips all hooks including this gate. This is intentional. There are legitimate cases -- documentation-only changes, CI configuration updates, emergency hotfixes -- where bypassing makes sense. The gate is a guardrail, not a jail. In practice, having to type --no-verify is enough friction to make you pause and think about whether you really want to skip the review.

Why a flag file instead of running the review directly in the hook?

Running an AI-powered multi-domain review inside a git hook creates two problems: the hook becomes blocking (no terminal output, no ability to interact), and if the review takes more than a few seconds, git may time out or the developer will kill it. The decoupled approach -- review first, push later -- preserves the interactive review experience while still enforcing the gate.

Does this work with multiple developers on a team?

The flag file is local to each developer's machine (/tmp/), so each developer must run their own pre-push review. The .githooks/pre-push script is committed to the repo, so every developer who configures core.hooksPath gets the gate automatically. The skill definition can also be committed to .claude/skills/ for teams using Claude Code.

What if I change files after running /pre-push but before pushing?

If your changes are within the 30-minute TTL window, the gate will let you push. This is a deliberate tradeoff: the TTL trusts that small fixes after a review are acceptable. For large changes, the 30-minute window naturally expires and forces a re-review. An enhancement would be to hash the git diff at review time and compare it at push time, but in practice the TTL approach is simpler and sufficient.

Can this pattern work without Claude Code?

Absolutely. The core pattern -- run a scoped review, write a timestamped flag file, check the flag in a git hook -- is tool-agnostic. You could implement the review step as a shell script that runs eslint --cache on changed files, checks for untracked imports with git ls-files, and runs tsc on only the affected TypeScript project references. The flag-file-with-TTL mechanism works the same regardless of what generates the flag.


Dinesh Challa is an AI Product Manager building production software with Claude Code. Follow him on LinkedIn.

Published April 11, 2026. Part of a series on Claude Code developer workflows, covering the tooling patterns behind shipping a production application with AI-assisted development.