Your First Git Hook: Stop Secrets Reaching a Commit
A pre-commit hook that scans what you're about to commit for API keys and refuses the commit if it finds one, with a fake key to test it.

This is the first hands-on post in a short series on guardrails for building with AI, and this one doesn’t need AI at all. It is a git hook that checks every commit for API keys and passwords before the commit is made, so a key that ends up in a file by accident stays on your machine and doesn’t end up on GitHub. If you let an AI assistant run git commands for you, the hook runs on those commits as well.
The problem
API keys end up in files by accident, for example pasted into a config file to test something, or a .env file that was never added to .gitignore, and once a key is in a commit that’s been pushed it’s in the history, so deleting the file in the next commit doesn’t remove it. The fix at that point is to cancel the key and make a new one.
A git hook is a script that git runs at certain points, and a pre-commit hook runs just before each commit. If the script ends with an error, git cancels the commit and nothing is saved, so the hook can check the files you’re committing and stop the commit when it finds a key.
The fix
Everything here is done in the terminal, as the hook goes in a folder that Finder and most code editors hide, so you won’t see it there and don’t need to. These steps are for a Mac.
1. Install a secret scanner. I use Betterleaks, and with Homebrew it installs with:
brew install betterleaks
If you get command not found: brew, install Homebrew first from its site. The Betterleaks GitHub page lists other ways to install it, including for Linux and Windows. To check it installed, run betterleaks version, which prints the version number.
2. Open your project in Terminal. Open Terminal and type cd followed by the path to your project folder, then press Return. For example, if your project is a folder called my-site inside a Projects folder in your home folder:
cd ~/Projects/my-site
~ is short for your home folder, and pressing Tab partway through a folder name fills in the rest. To check you’re in the right place, pwd prints the folder you’re in and ls lists what’s in it. The project needs to be using git, which you can check with:
git status
If it says not a git repository, you may be in the wrong folder, or git isn’t set up for the project yet, and git init sets it up.
3. Check where git looks for hooks. Some tools move the hooks folder, so check first:
git config core.hooksPath
If nothing is printed, git uses the usual .git/hooks/ folder, so carry on to step 4. If it prints a folder, a tool is managing your hooks (for husky it prints .husky/_), so skip to “If a tool manages your hooks” below.
4. Check there isn’t a hook already.
ls .git/hooks/pre-commit
If it says No such file or directory, there isn’t one. If it lists the file, you already have a pre-commit hook and the next step would replace it, so stop here and look at what that hook does first.
5. Create the hook. Paste these 2 lines into Terminal and press Return. The first writes the hook file, and the second makes it executable so git can run it.
printf '#!/bin/sh\nbetterleaks git --staged --redact .\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
To check what it wrote, run this, which prints the 2 lines of the hook, #!/bin/sh and the betterleaks line:
cat .git/hooks/pre-commit
The hook only exists on this computer, as hooks in .git/hooks/ aren’t included when you push or clone a project, so if you work on the project somewhere else, run steps 3 to 5 again there.
If a tool manages your hooks
Husky, which my own projects use, keeps hooks in a .husky/ folder that’s committed with the project, so every copy of the project gets them, and it points git at .husky/_ so git doesn’t look in .git/hooks/ at all.
So with husky, open your project in your code editor and open the pre-commit file in the .husky folder. It may show in your editor’s file list, though Finder hides it. Add this on a new line at the end, and save:
betterleaks git --staged --redact .
If there’s no pre-commit file in .husky, create one with just that line in it. If a different tool printed its folder in step 3, add the same line to that tool’s pre-commit hook, referring to its documentation as needed.
Why it works
--staged tells the scanner to only look at the changes you’ve staged with git add, which is what’s about to be committed, and if it finds something that matches one of its rules it exits with an error, so git cancels the commit. --redact hides the secret in the output, so the key isn’t shown in your terminal, or in an AI assistant’s session if it ran the commit.
Check it worked
Make a file with a fake GitHub token in it. This command makes one with random characters, so it’s the right shape for the scanner but isn’t a real key:
echo "GITHUB_TOKEN=ghp_$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 36)" > test-key.txt
Then stage it and try to commit:
git add test-key.txt
git commit -m "test the hook"
The scanner ends with leaks found: 1 and there’s no commit. To confirm it, run git status, and test-key.txt is still listed under “Changes to be committed”, as it was never committed. Then unstage and delete the test file:
git rm --cached test-key.txt
rm test-key.txt
The next normal commit goes through as usual, with no leaks found printed before it. If you commit from an app as well, such as your code editor’s git panel, try the test there too, as an app might not run the hook the same way as Terminal does.
Things to know
If the hook stops a commit and you’re not sure which file it was, Betterleaks version 1 doesn’t name the file unless you ask for a report. This writes one and prints the file and line, with the key still hidden:
betterleaks git --staged --redact -r /tmp/leaks.json .
grep -E '"(File|StartLine)"' /tmp/leaks.json
From version 2 the file is named in the output already, so you won’t need this, and betterleaks version from step 1 tells you which one you have.
The hook can be skipped with git commit --no-verify, which is sometimes needed, so it doesn’t stop a commit that someone decides to force through, and an AI assistant can add that flag too. The next post in the series covers a Claude Code hook that can stop it from running commands like that.
The hook only checks what you’re about to commit, so it won’t find a key that’s already in your history. To check the history, betterleaks git . scans every commit, and if it finds a key, cancel it with the service that issued it and make a new one, especially if those commits have been pushed.
It’s also worth checking your .env file is ignored by git, so the file with your real keys never gets staged at all:
git check-ignore .env
If it prints .env it’s ignored, and if it prints nothing, add a line with .env to your .gitignore file. If .env was committed before, adding it to .gitignore won’t remove it from the history, so check the history as above. I hope this helps if you give it a go.