{"slug": "a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you", "title": "A Local AI Pre-Commit Hook That Blocks Secrets Without Annoying You", "summary": "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.", "body_md": "My regex secret scanner once blocked a commit because a test file contained the string `sk_test_EXAMPLE_KEY_DO_NOT_USE`\n\n. 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.\n\nThe standard fix is an allowlist file that grows forever, plus developers who learn to type `git commit --no-verify`\n\nfrom muscle memory. Once people bypass the hook by habit, the scanner is decoration.\n\nSo 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.\n\nStage 1 is a deliberately paranoid regex pass over staged changes: high-entropy strings, known key prefixes, `PRIVATE KEY`\n\nblocks, 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`\n\nblocks the commit, `FALSE_POSITIVE`\n\nlets it through. Most commits never trigger stage 1 at all, so most commits pay zero latency.\n\n`.git/hooks/pre-commit`\n\n(or wire it through your hook manager of choice):\n\n``` bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nMODEL=\"${SECRET_HOOK_MODEL:-qwen2.5-coder:7b}\"\n\n# Stage 1: cheap and paranoid. Wide patterns, we WANT false positives here.\nPATTERNS=(\n  'AKIA[0-9A-Z]{16}'                      # AWS access key\n  '-----BEGIN( RSA| EC| OPENSSH)? PRIVATE KEY-----'\n  '(api[_-]?key|secret|token|passwd|password)[\"'\"'\"']?\\s*[:=]\\s*[\"'\"'\"'][^\"'\"'\"']{16,}'\n  '0x[a-fA-F0-9]{64}'                     # possible EVM private key\n  '[A-Za-z0-9+/]{40,}={0,2}'              # high-entropy base64-ish\n)\n\nflagged=()\nwhile IFS= read -r file; do\n  [[ -f \"$file\" ]] || continue\n  for p in \"${PATTERNS[@]}\"; do\n    if git show \":$file\" | grep -qE \"$p\"; then\n      flagged+=(\"$file\")\n      break\n    fi\n  done\ndone < <(git diff --cached --name-only --diff-filter=ACM)\n\n[[ ${#flagged[@]} -eq 0 ]] && exit 0   # fast path: nothing suspicious\n\n# Stage 2: ask the local model about each flagged file's staged content.\nblock=0\nfor file in \"${flagged[@]}\"; do\n  verdict=$(git show \":$file\" | ollama run \"$MODEL\" \"$(cat <<'PROMPT'\nYou review a file staged for a git commit. Decide if it contains a REAL\ncredential that must not be committed.\n\nREAL secrets: live API keys, private keys (including 0x-prefixed 64-hex\nEVM keys), tokens, passwords, connection strings with embedded passwords.\n\nNOT secrets: placeholders (YOUR_KEY_HERE, xxx, changeme), documented\nexample keys, test fixtures clearly labeled as fake, public addresses,\nhashes of public data, template variables like ${API_KEY}, lockfile\nintegrity hashes.\n\nFirst line of your answer must be exactly SECRET or FALSE_POSITIVE.\nSecond line: one short reason.\nPROMPT\n)\")\n  if [[ \"$verdict\" == SECRET* ]]; then\n    echo \"BLOCKED: $file\"\n    echo \"$verdict\" | sed -n '2p' | sed 's/^/  reason: /'\n    block=1\n  fi\ndone\n\nif [[ $block -eq 1 ]]; then\n  echo \"\"\n  echo \"Commit blocked. If this is wrong, re-run with SECRET_HOOK_MODEL\"\n  echo \"set to a bigger model, or use --no-verify and accept the risk.\"\n  exit 1\nfi\nexit 0\n```\n\nTwo details in there matter more than they look.\n\n**Scan the staged content, not the working tree.** `git show \":$file\"`\n\nreads 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.\n\n**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.\n\nNotice 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.\n\nThe `0x`\n\n+ 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...`\n\napart from `KNOWN_TX_HASH = 0x...`\n\n. 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).\n\n**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`\n\nor 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.\n\n**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/`\n\nand the variable is called `FAKE_KEY`\n\n\". For this job the model runs rarely, so I pay for the bigger one.\n\n**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`\n\n. A scanner people trust and obey beats a stricter one everybody bypasses.\n\n**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.\n\nI'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.\n\nWhat's your current secret-scanning setup, and be honest, how often do you bypass it?", "url": "https://wpnews.pro/news/a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you", "canonical_source": "https://dev.to/pavelespitia/a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you-39of", "published_at": "2026-08-03 16:32:16+00:00", "updated_at": "2026-08-03 16:44:17.600935+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "ai-tools"], "entities": ["Ollama", "qwen2.5-coder", "Etherscan"], "alternates": {"html": "https://wpnews.pro/news/a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you", "markdown": "https://wpnews.pro/news/a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you.md", "text": "https://wpnews.pro/news/a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you.txt", "jsonld": "https://wpnews.pro/news/a-local-ai-pre-commit-hook-that-blocks-secrets-without-annoying-you.jsonld"}}