← All posts
·4 min read·GuardLayer

Catch API Keys Before They Hit GitHub

SecretsGitNext.jsCI

A leaked API key on a public GitHub repo is scraped and abused within minutes, and deleting it later doesn't help — it's in git history forever. Catch secrets in two layers: a local pre-commit hook that blocks the commit before it's created (true prevention), plus a scanner that checks every push and flags anything that slipped through, so you can rotate it before it's exploited.

There's a live market of bots doing exactly one thing: watching public GitHub events and grabbing freshly committed API keys — Stripe, AWS, OpenAI, Supabase. The window between "I pushed" and "someone is using my key" is measured in minutes, sometimes seconds. So the real goal isn't tidy code; it's making sure a secret never reaches a place it can be scraped from — and if one does, catching it fast enough to rotate before it's abused.

Here's the mistake, and why it's stubborn:

import Stripe from "stripe";

// Pasted in "just to test" — now one commit from being public.
const STRIPE_SECRET_KEY = "sk_live_51Nk2pLZvKq9rT4xBwHc8mYdQ";

export const stripe = new Stripe(STRIPE_SECRET_KEY);

This is what GuardLayer reports on that file — live engine output, with the fix inline:

guardlayer scan · lib/stripe.tsLive engine output
Check failed
75/100 · B
  • Criticallib/stripe.ts:4

    Hardcoded secret in source

    Move the secret to an environment variable, purge it from git history (e.g. git filter-repo / BFG), and rotate it at the provider.

The trouble is that deleting the line later doesn't remove the key — git keeps every past commit. So prevention has to happen early, and detection has to happen fast.

The honest picture: where can you actually stop a secret?

Be clear about what stops a leak at which stage, because tools get marketed loosely here:

  • Pre-commit / pre-push (local git hook): the only place you can block a secret before it leaves your machine. This is true prevention — the commit or push is rejected.
  • On push (a scanner watching the repo): runs after the code reaches GitHub and flags what got through, within seconds. It can't un-push a commit — no GitHub app can — but it tells you immediately so you rotate before the key is exploited, and it stops the secret from advancing further (into a PR merge, into main, into a deploy).

You want both layers. The local hook catches the obvious cases for free; the push scan is the backstop for everything the hook missed or that was bypassed with --no-verify.

Layer 1: block the commit locally

Add a pre-commit hook that scans staged changes for secret patterns and refuses the commit. gitleaks is the common choice:

# .git/hooks/pre-commit  (or wire it via the `pre-commit` framework)
#!/bin/sh
gitleaks protect --staged --redact --verbose || {
  echo "🚫 Secret detected in staged changes — commit blocked."
  exit 1
}

This is genuine prevention: the key never enters even a local commit. The catch is that hooks are per-machine and skippable (git commit --no-verify), so you can't rely on them alone across a team or your future rushed self.

Layer 2: scan every push as the backstop

This is where a repo-connected scanner earns its place. GuardLayer runs on every push, reads the diff against your whole codebase, and flags a hardcoded key (and a secret behind a NEXT_PUBLIC_ prefix, which a plain secret scanner often misses) as critical — posting a pass/fail check on the commit and PR. To be precise about what that does: it catches and reports the leak in seconds; it doesn't reject the raw push. If you want a failing check to actually block the code from merging into main, make it a required status check in your branch protection — otherwise it's an immediate alert, not a hard gate.

Either way, the value is speed: you learn a key shipped while there's still time to rotate it.

If a key already reached GitHub

Assume it's compromised the moment it's public. Then:

  1. Rotate immediately — revoke it at the provider and issue a new one. Do this first, before cleanup.
  2. Move the new key server-side — into an untracked .env, read via process.env in a server-only module (keeping secrets out of Next.js).
  3. Purge historygit filter-repo or BFG, then force-push. The service_role key and any provider key must be scrubbed from every past commit, not just the latest.

FAQ

Can anything actually stop me from pushing a secret to GitHub? Only a local git hook (pre-commit or pre-push) can block it before it leaves your machine. A repo-connected scanner runs after the push and catches it within seconds — fast enough to rotate — but it can't reject the push itself. Use both.

Isn't deleting the commit enough if I catch it fast? No. Even a force-push doesn't guarantee removal from forks, caches, or anything already scraped. Always rotate the key; treat cleanup as secondary.

Does GuardLayer replace gitleaks? They're complementary. gitleaks blocks the commit locally; GuardLayer scans every push as a server-side backstop and also catches Next.js/Supabase-specific leaks (like a NEXT_PUBLIC_ secret) that generic secret scanners don't flag.

How fast are leaked keys really exploited? Public-repo secrets are routinely abused within minutes — automated scrapers watch GitHub's public event stream continuously. Speed of detection and rotation is the whole game.

Catch this before it ships — free

GuardLayer scans every push for this and 19 other Next.js + Supabase issues, with the exact fix inline.

No signup, no card — your code is scanned in memory and never stored.

Keep reading