<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul Harvey</title>
    <description>The latest articles on DEV Community by Paul Harvey (@harvest316).</description>
    <link>https://dev.to/harvest316</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3946958%2Fd42a007e-d29b-455e-a64b-72dbae567683.png</url>
      <title>DEV Community: Paul Harvey</title>
      <link>https://dev.to/harvest316</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harvest316"/>
    <language>en</language>
    <item>
      <title>The Pre-Commit Hook That Catches API Keys Before They Hit Git</title>
      <dc:creator>Paul Harvey</dc:creator>
      <pubDate>Sat, 23 May 2026 03:22:30 +0000</pubDate>
      <link>https://dev.to/harvest316/the-pre-commit-hook-that-catches-api-keys-before-they-hit-git-4h22</link>
      <guid>https://dev.to/harvest316/the-pre-commit-hook-that-catches-api-keys-before-they-hit-git-4h22</guid>
      <description>&lt;h2&gt;
  
  
  The problem: secrets in git are forever
&lt;/h2&gt;

&lt;p&gt;You know the drill. A developer hardcodes a Stripe secret key to test a webhook handler locally. They commit. They push. Maybe they catch it themselves and run &lt;code&gt;git rm&lt;/code&gt;. Problem solved, right?&lt;br&gt;
Wrong. The key is still in your git history. Anyone who clones the repo can run &lt;code&gt;git log -p&lt;/code&gt; and find it. Bots scrape GitHub for exactly this pattern. GitGuardian reported over &lt;strong&gt;10 million secrets&lt;/strong&gt; detected in public commits in 2023 alone, and the number keeps climbing.&lt;br&gt;
Scrubbing secrets from git history means &lt;code&gt;git filter-branch&lt;/code&gt; or BFG Repo-Cleaner, force-pushing to every remote, and hoping nobody already pulled the old history. If the key reached a public repo for even a few minutes, you need to rotate it. For AWS, that means updating every service, Lambda, and CI pipeline that uses it. For Stripe, that means regenerating keys and redeploying payment infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real cost is not the cleanup.&lt;/strong&gt; It is the blast radius. A leaked AWS key can rack up tens of thousands in compute charges before you notice. A leaked Stripe key gives an attacker access to your customer payment data. Prevention is not optional.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: a POSIX pre-commit hook
&lt;/h2&gt;

&lt;p&gt;A git pre-commit hook runs automatically before every commit. If it exits with a non-zero status, the commit is blocked. The strategy: scan every staged file for patterns that look like secrets, and refuse to commit if anything matches.&lt;br&gt;
Here is the skeleton. This goes in &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; (or use a symlink from a checked-in &lt;code&gt;scripts/&lt;/code&gt; directory so every developer on the team gets it).&lt;br&gt;
Shell&lt;br&gt;
.git/hooks/pre-commit&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Pre-commit hook: block secrets from reaching git history&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;STAGED_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STAGED_FILES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;FOUND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$STAGED_FILES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;# Skip binary files&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;file &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"binary"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
continue
  fi&lt;/span&gt;

  &lt;span class="c"&gt;# Get only the staged content (not working tree)&lt;/span&gt;
  &lt;span class="nv"&gt;CONTENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git show &lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

  &lt;span class="c"&gt;# Check for known secret patterns&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | check_patterns &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nv"&gt;FOUND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FOUND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"COMMIT BLOCKED: potential secrets detected."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Add a pii-ok comment to suppress false positives."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key detail: we use &lt;code&gt;git show ":$file"&lt;/code&gt; to read the staged content, not the working tree. This prevents false negatives where a developer stages a file with a secret, then removes it from the working copy but does not re-stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern matching: what to look for
&lt;/h2&gt;

&lt;p&gt;The core of the hook is a set of regular expressions that match known secret formats. These are not hypothetical patterns. They are extracted from real-world key formats.&lt;br&gt;
Shell&lt;br&gt;
Pattern definitions&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;check_patterns&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

  &lt;span class="c"&gt;# AWS Access Key ID&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'AKIA[0-9A-Z]{16}'&lt;/span&gt; | filter_suppressed&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [AWS] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: AWS Access Key ID"&lt;/span&gt;
&lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# Stripe secret key&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'sk_(live|test)_[0-9a-zA-Z]{24,}'&lt;/span&gt; | filter_suppressed&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [STRIPE] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: Stripe secret key"&lt;/span&gt;
&lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# Stripe restricted key&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'rk_(live|test)_[0-9a-zA-Z]{24,}'&lt;/span&gt; | filter_suppressed&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [STRIPE] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: Stripe restricted key"&lt;/span&gt;
&lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# GitHub personal access token&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'ghp_[0-9a-zA-Z]{36}'&lt;/span&gt; | filter_suppressed&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [GITHUB] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: GitHub PAT"&lt;/span&gt;
&lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# Generic high-entropy strings (API keys, tokens)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"['&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;][0-9a-zA-Z]{32,}['&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt; | filter_suppressed&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ENTROPY] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: high-entropy string (&amp;gt;=32 chars)"&lt;/span&gt;
&lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi

  return&lt;/span&gt; &lt;span class="nv"&gt;$matched&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The high-entropy check at the end is the catch-all. Any quoted string of 32+ alphanumeric characters is flagged. This catches tokens, API keys, and secrets that do not match a known vendor pattern. It will also flag some legitimate values like UUIDs and hashes, which is where the suppression pragma comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pii-ok pragma: handling false positives
&lt;/h2&gt;

&lt;p&gt;Every secret scanner produces false positives. A SHA-256 hash in a test fixture. A base64-encoded public key. A long CSS class name generated by a build tool. If there is no escape hatch, developers will disable the hook entirely, which defeats the purpose.&lt;br&gt;
The solution is a suppression comment: &lt;code&gt;pii-ok&lt;/code&gt;. If a line contains this marker, the scanner skips it.&lt;br&gt;
Shell&lt;br&gt;
Suppression filter&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;filter_suppressed&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# Remove lines containing the suppression marker&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"pii-ok"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice it looks like this:&lt;br&gt;
JavaScript&lt;br&gt;
Example usage in code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This SHA-256 is a test fixture, not a secret&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EXPECTED_HASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a1b2c3d4e5f6...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// pii-ok&lt;/span&gt;

&lt;span class="c1"&gt;// This WILL be caught (no pragma)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STRIPE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk_live_abc123...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is simple: if you know a value is not a secret, add &lt;code&gt;pii-ok&lt;/code&gt; on the same line. If you are not sure, leave it off and let the hook flag it. The inconvenience of a false positive is nothing compared to the cost of a leaked key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further: .htaccess and env files
&lt;/h2&gt;

&lt;p&gt;The pattern-matching approach extends to other dangerous file types. &lt;code&gt;.htaccess&lt;/code&gt; files with &lt;code&gt;SetEnv&lt;/code&gt; directives often contain database passwords. &lt;code&gt;.env&lt;/code&gt; files are secrets by definition. Your hook should flag both.&lt;br&gt;
Shell&lt;br&gt;
Additional checks&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Block .env files entirely&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'\.env$'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ENV] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: .env files must be .gitignored"&lt;/span&gt;
  &lt;span class="nv"&gt;FOUND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;continue
fi&lt;/span&gt;

&lt;span class="c"&gt;# Flag SetEnv with real values in .htaccess&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'\.htaccess$'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'SetEnv\s+\S+\s+\S+'&lt;/span&gt; | filter_suppressed&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [HTACCESS] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: SetEnv with real values"&lt;/span&gt;
&lt;span class="nv"&gt;FOUND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The convention: commit &lt;code&gt;.env.example&lt;/code&gt; with &lt;code&gt;&amp;lt;REPLACE_ME&amp;gt;&lt;/code&gt; placeholders. The real &lt;code&gt;.env&lt;/code&gt; stays in &lt;code&gt;.gitignore&lt;/code&gt;. Same for &lt;code&gt;.htaccess&lt;/code&gt; files that contain credentials -- commit a sanitized version, keep the real one out of version control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI layer: catching what regex cannot
&lt;/h2&gt;

&lt;p&gt;Pattern matching catches known secret formats. But what about a database connection string with an embedded password? Or a hardcoded JWT that does not match any vendor prefix? Or code that is technically functional but has a SQL injection vulnerability?&lt;br&gt;
This is where an &lt;strong&gt;LLM-powered code review gate&lt;/strong&gt; comes in. The idea: after the regex-based pre-commit hook passes, run a second pass that sends the diff to an LLM and asks it to identify security concerns. The model can catch patterns that regex never will -- SQL injection, logic errors that expose data, hardcoded credentials in unusual formats, and more.&lt;br&gt;
The review gate reads your staged diff, sends it to an LLM with a security-focused system prompt, and blocks the commit if the model identifies high-severity issues. It complements the fast, deterministic regex hook with the contextual understanding of a language model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it a team standard
&lt;/h2&gt;

&lt;p&gt;A pre-commit hook that lives in &lt;code&gt;.git/hooks/&lt;/code&gt; only works on one machine. To make it a team-wide standard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check the hook into the repo&lt;/strong&gt; under &lt;code&gt;scripts/pre-commit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a setup script&lt;/strong&gt; that symlinks it: &lt;code&gt;ln -sf ../../scripts/pre-commit .git/hooks/pre-commit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the pii-ok pragma&lt;/strong&gt; so developers know how to suppress false positives without disabling the hook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the same patterns in CI&lt;/strong&gt; as a backup, because developers can skip hooks with &lt;code&gt;--no-verify&lt;/code&gt;
The hook must be fast. If it takes more than a second or two, developers will bypass it. Pure POSIX shell with &lt;code&gt;grep&lt;/code&gt; keeps it under 200ms even on large commits. The AI review gate is optional and can be configured to run only on push or in CI if latency is a concern.
&amp;lt;!-- CTA Box --&amp;gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code Kit&lt;/p&gt;

&lt;h3&gt;
  
  
  The complete pre-commit safety system, ready to drop in
&lt;/h3&gt;

&lt;p&gt;The patterns in this post are a starting point. Claude Code Kit is the production-hardened version: a complete PII scanner, AI-powered code review gate, pre-commit hooks covering 15+ secret formats, and CLAUDE.md templates for teams using AI coding assistants. 53 tests. Zero dependencies. Pure POSIX shell.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PII scanner with 15+ patterns&lt;/li&gt;
&lt;li&gt;AI code review gate (LLM-powered)&lt;/li&gt;
&lt;li&gt;pii-ok pragma suppression&lt;/li&gt;
&lt;li&gt;CLAUDE.md team templates&lt;/li&gt;
&lt;li&gt;.env / .htaccess protection&lt;/li&gt;
&lt;li&gt;53 passing tests&lt;/li&gt;
&lt;li&gt;Zero dependencies&lt;/li&gt;
&lt;li&gt;POSIX shell -- works everywhere
&lt;a href="https://aiclarityau.gumroad.com/l/claude-code-kit" rel="noopener noreferrer"&gt;Get Claude Code Kit -- $29&lt;/a&gt;
&lt;a href="//../#bundles"&gt;Full Stack Bundle -- $149&lt;/a&gt;
One-time purchase. No subscription.
## What to do next
If you do nothing else today, add the basic pattern-matching hook from this post to your repositories. It takes five minutes, it costs nothing, and it will save you from at least one costly key rotation.
For a production-ready implementation with broader pattern coverage, the AI review gate, team templates, and a full test suite, &lt;a href="https://aiclarityau.gumroad.com/l/claude-code-kit" rel="noopener noreferrer"&gt;Claude Code Kit&lt;/a&gt; has it all packaged up and documented. It is $29, it is a one-time purchase, and every line of source code is included. No binaries, no obfuscation, no vendor lock-in.
Your git history should contain your work, not your secrets.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
