How to check for secrets in a git repository
Run git grep -n -I -E 'AKIA[0-9A-Z]{16}' $(git rev-list --all) to search every commit rather than the working tree. A key removed three commits ago still sits in the old blob, so git grep HEAD finds nothing while the history scan prints the file, the line and the commit that holds it.
Why check this
A credential that reached a commit is published the moment the branch is pushed, and deleting the line does not take it back. Anyone with clone access reads it out of the old blob in one command. Run this before a repository changes visibility, and as a gate on the pull request that adds an integration.
The failure it prevents is the quiet one: a review approves the diff that removes a key, everyone treats the key as gone, and it stays readable until the credential is rotated.
Prerequisites
- git.
git grepaccepts a list of commits as its final arguments, which is what turns it into a history scanner. Verified here with 2.41.0. - A full clone.
git clone --depth 1fetches one commit, and every command below then reports a clean repository that is not clean. - A repository with a known planted secret, so you can tell a working scan from a scan that matched nothing. Build one in a scratch directory:
mkdir secretdemo && cd secretdemo && git init -q -b main
printf "export const config = {\n region: 'eu-central-1',\n accessKeyId: 'AKIAIOSFODNN7EXAMPLE',\n secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n};\n" > config.js
git add -A && git commit -q -m "add s3 config"
printf "export const config = {\n region: 'eu-central-1',\n accessKeyId: process.env.AWS_ACCESS_KEY_ID,\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\n};\n" > config.js
git commit -q -am "move credentials to environment variables"
printf '# demo service\n' > README.md && git add -A && git commit -q -m "add readme"
- The values above are AWS documentation samples and authorise nothing.
Steps
- Step 1.
Scan the current commit. This is what a reviewer sees and it is the answer most people stop at.
git grep -n -E 'AKIA[0-9A-Z]{16}' HEAD; echo "exit=$?"exit=1No output and exit code 1.
git grepexits 1 when nothing matched, the same asgrep. - Step 2.
Scan every commit reachable from every branch and tag.
git grep -n -I -E 'AKIA[0-9A-Z]{16}' $(git rev-list --all)1961aa65cd7fe542d7c48ec9de1ed3164110d5cc:config.js:3: accessKeyId: 'AKIAIOSFODNN7EXAMPLE',Three fields before the match: the commit, the path inside that commit, the line number.
-Iskips binary blobs, which otherwise flood the result with base64-looking noise. - Step 3.
Name the commits where that string entered and left.
git log --oneline -S 'AKIAIOSFODNN7EXAMPLE' --all3a7c7b1 move credentials to environment variables 1961aa6 add s3 config-Scounts occurrences and reports each commit that changed the count, so the removal appears alongside the introduction. The second line is the commit to date the rotation from. - Step 4.
Read the old file straight out of the object database, with no checkout.
git cat-file -p 99c091fexport const config = { region: 'eu-central-1', accessKeyId: 'AKIAIOSFODNN7EXAMPLE', secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', };The blob id comes from the
index 99c091f..286be46line ofgit log -p. Anyone holding a clone can run this, which is why a deleted key is a rotated key or an exposed one. - Step 5.
Search for the assignment shape as well as the key shape. The regex in step 2 matched the access key id and missed the value beside it.
git grep -n -I -i -E "(secret|token|password|api_?key)[a-z_]*\s*[:=]\s*[\"'][^\"']{8,}" $(git rev-list --all)1961aa65cd7fe542d7c48ec9de1ed3164110d5cc:config.js:4: secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',The
[a-z_]*after the keyword is what makes it hit. Without it the pattern requiressecretto be followed by a colon, andsecretAccessKey:never matches. - Step 6.
Collapse the duplicates. Run the same scan on a repository with real history, in this case the h2check repository at 82 commits.
git grep -n -I -i -E "(secret|token|password|api_?key)[a-z_]*\s*[:=]\s*[\"'][^\"']{8,}" $(git rev-list --all) | cut -d: -f2- | sort -u | wc -l11The raw scan printed 334 lines. Eleven are distinct file, line and text triples; the rest are the same lines carried forward by later commits.
cut -d: -f2-drops the commit id, which is the field that makes them look different. - Step 7.
Include commits that no branch points at any more.
git grep -n -I -E "xoxb-[0-9]{10}" $(git rev-list --all --reflog)7d6096847718c0a237901366cb9be00da5ab78fa:notify.js:1:export const SLACK = "xoxb-4242424242-abcdefghijklmnop";The same pattern without
--reflogexits 1 and prints nothing. The commit was undone withgit reset --hard, so no ref reaches it and only the reflog still names it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Empty output, exit 1 | Nothing in the scanned commits matched | Widen the pattern, then confirm the clone is not shallow. |
| A hit on a commit that is not HEAD | The value is in history and not in the current code | Rotate the credential. Removing the commit does not reach clones. |
| The same line under many commit ids | One occurrence carried forward | Drop the commit field and sort unique before counting, as in step 6. |
| A hit only under --reflog | An unreachable local commit holds it | It never left this machine if it was never pushed. Confirm against the remote. |
| A hit on a file that is not in the working tree | The file was deleted | Check the deleting commit, then rotate anything it held. |
Common mistakes
What to check next
- How to check for secrets in environment variables: the same values once they reach a running process.
- How to check if env variable exists: confirms the replacement variables are actually set after the key leaves the code.
- How to check config drift between environments: finds the environment file where the old key survived.
- How to check npm packages for vulnerabilities: the other repository scan that belongs on the same gate.
- Pull request checklist: where this check sits in a review.
FAQ
How do I check for secrets in a git repo without installing anything?
Use git grep with a commit list, as in step 2. Nothing beyond git is needed, and it ran in seconds on an 82-commit repository here. A dedicated scanner adds a maintained rule set, which a hand-written pattern does not have.
Does removing the file remove the secret?
No. The blob stays in the object database and git cat-file -p reads it back, as step 4 shows. Rewriting history with git filter-repo removes it locally, and every existing clone keeps its copy. Rotate the credential.
Why does the scan take so long on a large repository?
git rev-list --all expands to one argument per commit, and git searches each commit's tree. Limit the range with --since, or scan one branch, when a full pass is too slow for a hook.
What patterns should I start with?
The provider formats you use, plus the assignment shape from step 5. AKIA and 16 uppercase alphanumerics is an AWS access key id, xoxb- starts a Slack bot token, -----BEGIN opens a private key.
Does this find secrets in a shallow clone?
No. git clone --depth 1 fetches one commit, so git rev-list --all returns one id and the scan reports a clean repository. Run git rev-parse --is-shallow-repository first, and fetch with --unshallow before trusting the result.
Verified
Verified by Maks Vernygit 2.41.0.windows.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate8 minpublished updated Maks Verny