DEV Community

piman
piman

Posted on

I built a pre-push git hook that catches leaked secrets before they hit GitHub

$ git push origin main
# A few seconds later, email from AWS
# Next morning: $8,000
Enter fullscreen mode Exit fullscreen mode

Leaked API keys are a solved problem — in theory. In practice, they keep happening. GitHub says exposed credentials are scraped by bots within seconds of being pushed.

The usual advice is "use a secrets manager" or "scan your repo with a tool." But the best time to catch a leaked secret is before it ever leaves your machine.

That's why I built push-sentinel — a zero-dependency CLI that sits in your pre-push hook and scans the exact diff being pushed.


Install in one command

npx --yes --prefer-online push-sentinel@latest install
Enter fullscreen mode Exit fullscreen mode

That's it. From now on, every git push runs the scan automatically.


What it looks like

Clean push:

[push-sentinel] ✓ No secrets detected.
Enter fullscreen mode Exit fullscreen mode

When something is found:

[push-sentinel] ⚠ Potential secrets found:

  [HIGH] src/config.ts:12
  AKIAIO...
  → Risk: Full access to AWS resources. Attacker can create/delete
           instances, incur charges, or exfiltrate data.
  → To ignore this line: push-sentinel ignore src/config.ts:12

  Push continues. Double-check before sharing.
Enter fullscreen mode Exit fullscreen mode

By default it warns and lets the push through — no friction, no temptation to reach for --no-verify.


What it detects

Pattern Severity
Private Key (RSA, EC, OPENSSH, DSA, PKCS#8) 🔴 HIGH
AWS Access Key (AKIA...) 🔴 HIGH
AWS Secret Key (variable name + entropy) 🔴 HIGH
GitHub Token (ghp_, github_pat_) 🔴 HIGH
Anthropic API Key (sk-ant-...) 🟡 MEDIUM
OpenAI API Key (sk-...) 🟡 MEDIUM
Generic API Key (variable name + high entropy) 🟢 LOW
.env file committed 🟡 MEDIUM

The entropy check on generic keys keeps false positives low — short or repetitive strings are skipped even if the variable name matches.


False positives? One command to ignore

push-sentinel ignore src/config.ts:12          # ignore a specific line
push-sentinel ignore --pattern OPENAI_API_KEY  # ignore a pattern everywhere
push-sentinel ignore --list                    # see all rules
push-sentinel ignore --remove OPENAI_API_KEY   # remove a rule
Enter fullscreen mode Exit fullscreen mode

Rules go into .push-sentinel-ignore at your repo root.


Why warning-only by default?

Hard blocks feel safer, but they train people to use --no-verify. A warning at push time is early enough to catch real accidents, and people actually leave it installed.

If you want hard blocking for HIGH findings:

# In .git/hooks/pre-push, change the scan line to:
npx --yes --prefer-online push-sentinel@latest scan --local-sha "$local_sha" --remote-sha "$remote_sha" --block-on-high
Enter fullscreen mode Exit fullscreen mode

Team or org-wide? Use the GitHub Action

For enforcing secret scanning across PRs without relying on every developer having the hook installed, there's also a GitHub Action:

- uses: Pmaind/push-sentinel-action@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

It blocks merges on HIGH findings and posts a PR comment with a findings table.


How it works under the hood

Git passes pushed ref info to the pre-push hook via stdin:

<local-ref> <local-sha> <remote-ref> <remote-sha>
Enter fullscreen mode Exit fullscreen mode

push-sentinel uses those SHAs to compute the exact range of commits being pushed (git log remoteSha..localSha -p), so it only scans what's actually new. It handles edge cases too:

  • New branch (no remote SHA): scans commits not yet reachable from any remote
  • Ref deletion (zero SHA for local): skips the scan, nothing is being pushed
  • Manual scan (no SHAs): falls back through @{u}..HEAD → staged → working tree → last commit

Zero dependencies

The entire tool uses only Node.js stdlib — no node_modules, no supply chain risk. Node.js >= 16 required.


Feedback, false positive reports, and PRs welcome.

Top comments (0)