cd /news/developer-tools/a-local-ai-pre-commit-hook-that-bloc… · home topics developer-tools article
[ARTICLE · art-84952] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

A Local AI Pre-Commit Hook That Blocks Secrets Without Annoying You

A developer has built a two-stage pre-commit hook that combines a regex scanner with a local LLM to block secrets without annoying developers. The hook first flags suspicious patterns, then uses Ollama running qwen2.5-coder to classify whether the flagged content is a real secret, keeping the process fast and private. The approach addresses the shortcomings of regex-only scanning, which often produces false positives and misses real secrets.

read6 min views1 publishedAug 3, 2026

My regex secret scanner once blocked a commit because a test file contained the string sk_test_EXAMPLE_KEY_DO_NOT_USE

. The same week, a colleague on another project committed a real Etherscan API key inside a hardcoded URL, and no scanner caught it because it didn't match any known key format. That pair of failures sums up regex-based secret scanning: loud where it doesn't matter, quiet where it does.

The standard fix is an allowlist file that grows forever, plus developers who learn to type git commit --no-verify

from muscle memory. Once people bypass the hook by habit, the scanner is decoration.

So I tried something different: keep the regex scanner, but add a small local LLM as a second opinion. The regex stage decides what's worth looking at. The model decides whether it's actually a secret. Only flagged files ever reach the model, so the hook stays fast, and because the model is Ollama running on my own machine, no staged diff ever leaves my laptop. That last part is non-negotiable for me, sending your possibly-secret-containing diffs to a cloud API to check for secrets is a joke that writes itself.

Stage 1 is a deliberately paranoid regex pass over staged changes: high-entropy strings, known key prefixes, PRIVATE KEY

blocks, suspicious variable names. Stage 2 sends each flagged hunk, with a few lines of surrounding context, to qwen2.5-coder via Ollama with a classification prompt. Verdict SECRET

blocks the commit, FALSE_POSITIVE

lets it through. Most commits never trigger stage 1 at all, so most commits pay zero latency.

.git/hooks/pre-commit

(or wire it through your hook manager of choice):

#!/usr/bin/env bash
set -euo pipefail

MODEL="${SECRET_HOOK_MODEL:-qwen2.5-coder:7b}"

PATTERNS=(
  'AKIA[0-9A-Z]{16}'                      # AWS access key
  '-----BEGIN( RSA| EC| OPENSSH)? PRIVATE KEY-----'
  '(api[_-]?key|secret|token|passwd|password)["'"'"']?\s*[:=]\s*["'"'"'][^"'"'"']{16,}'
  '0x[a-fA-F0-9]{64}'                     # possible EVM private key
  '[A-Za-z0-9+/]{40,}={0,2}'              # high-entropy base64-ish
)

flagged=()
while IFS= read -r file; do
  [[ -f "$file" ]] || continue
  for p in "${PATTERNS[@]}"; do
    if git show ":$file" | grep -qE "$p"; then
      flagged+=("$file")
      break
    fi
  done
done < <(git diff --cached --name-only --diff-filter=ACM)

[[ ${#flagged[@]} -eq 0 ]] && exit 0   # fast path: nothing suspicious

block=0
for file in "${flagged[@]}"; do
  verdict=$(git show ":$file" | ollama run "$MODEL" "$(cat <<'PROMPT'
You review a file staged for a git commit. Decide if it contains a REAL
credential that must not be committed.

REAL secrets: live API keys, private keys (including 0x-prefixed 64-hex
EVM keys), tokens, passwords, connection strings with embedded passwords.

NOT secrets: placeholders (YOUR_KEY_HERE, xxx, changeme), documented
example keys, test fixtures clearly labeled as fake, public addresses,
hashes of public data, template variables like ${API_KEY}, lockfile
integrity hashes.

First line of your answer must be exactly SECRET or FALSE_POSITIVE.
Second line: one short reason.
PROMPT
)")
  if [[ "$verdict" == SECRET* ]]; then
    echo "BLOCKED: $file"
    echo "$verdict" | sed -n '2p' | sed 's/^/  reason: /'
    block=1
  fi
done

if [[ $block -eq 1 ]]; then
  echo ""
  echo "Commit blocked. If this is wrong, re-run with SECRET_HOOK_MODEL"
  echo "set to a bigger model, or use --no-verify and accept the risk."
  exit 1
fi
exit 0

Two details in there matter more than they look.

Scan the staged content, not the working tree. git show ":$file"

reads the index. If you scan the file on disk, you'll block commits over unstaged scratch content, and you'll miss the case where the secret is staged but already deleted from the working copy.

The exit-code contract is the whole interface. Exit 0 commits, exit 1 blocks, and the model's freeform text never decides anything by itself. I parse only the first line and demand it be one of two tokens. Small local models will occasionally produce a paragraph of hedging, forcing a machine-readable first line is what makes them usable in a pipeline.

Notice the prompt spends more words on what is NOT a secret than on what is. That's deliberate. The regex stage already guarantees everything the model sees looks secret-ish, so the model's real job is recognizing placeholders, fixtures, and templates. Framing it that way cut my false blocks dramatically. I learned this pattern building spectr-ai: small models do much better when you tell them what to exclude than when you ask them open-ended "is this dangerous?" questions.

The 0x

  • 64 hex chars pattern deserves a note for the Web3 crowd. A transaction hash, a storage slot, and a private key all look identical to a regex. The model can use the variable name and surrounding code to tell DEPLOYER_PRIVATE_KEY = 0x...

apart from KNOWN_TX_HASH = 0x...

. That single distinction is most of the value I get from this hook, because regex scanners either flag every 32-byte hex value in a blockchain codebase (unbearable) or none (useless).

Latency. On my machine (WSL2, mid-range GPU), a flagged file costs roughly two to four seconds with the 7b model once it's warm, more if Ollama has to load the model first. Clean commits pay nothing because stage 1 short-circuits. Commits that touch a .env.example

or a test fixture pay a few seconds. I find that acceptable, you might not, and if your team commits forty times an hour you should keep the model stage async or advisory.

1.5b vs 7b. I tried qwen2.5-coder:1.5b first because it responds almost instantly. It was too eager to please: it labeled obviously fake fixtures as SECRET often enough that I would have started bypassing my own hook, which defeats the entire purpose. The 7b is noticeably better at reading context like "this is in tests/fixtures/

and the variable is called FAKE_KEY

". For this job the model runs rarely, so I pay for the bigger one.

It can still be wrong both ways. A local 7b model is not a security boundary. A weird real key can slip through, and this hook is a complement to your platform-side scanning (GitHub push protection and friends), not a replacement. What it fixes is the human layer: the hook complains so rarely that when it does, I actually stop and look instead of reflexively reaching for --no-verify

. A scanner people trust and obey beats a stricter one everybody bypasses.

Determinism. LLM verdicts can flip between runs on borderline inputs. For a blocking hook I accept that, borderline cases are exactly the ones I want a human to re-examine anyway.

I've run this for a couple of months now. The regex stage fires a few times a week, the model overrules it almost every time, and the one time it said SECRET it was right: a real RPC URL with an embedded API key inside an old test I was resurrecting.

What's your current secret-scanning setup, and be honest, how often do you bypass it?

── more in #developer-tools 4 stories · sorted by recency
── more on @ollama 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-local-ai-pre-commi…] indexed:0 read:6min 2026-08-03 ·