Scanning for secrets before code goes public
Details are generalized: no hostnames, addresses, identifiers or credentials.
How this was built: I design, direct and review the work, and build it hands-on with Claude, an AI coding assistant. The decisions, and the homelab, are mine.
Pattern scanners are good at finding secrets that look like secrets. Before years of homelab scripts went into a repository, and before this site's repository went public, I needed a method for the ones that don't.
The context
I consolidated scattered homelab projects into one version-controlled monorepo, importing their git history. This website's repository is public, so anything pushed to it is published. Every file and every commit had to be clean before the first push. Removing a secret from the latest version of a file doesn't remove it from the history.
Why one scanner isn't enough
Tools like gitleaks recognize secrets by shape: known prefixes such as AKIA… or ghp_…, high-entropy strings, and assignments like password = "…". Real code also carries credentials with no shape at all. A short password passed straight into a function call looks like any other string. So does a key embedded in a URL, or a token left in a comment. A pattern scanner can't flag what it can't recognize.
The method
- Pattern scan. gitleaks over the working tree and every commit in history.
- Exact-value sweep. Take every secret actually stored on the machine (each service's credential file), of any length, and search for that literal value across all files and the full
git log -p. If you already know the secret, you don't need to guess its shape. The sweep reports matches without ever printing the value. - Classify by masked shape. Plenty of hits aren't secrets: IDs, IP addresses and time zones all look suspicious to a scanner. Review them masked, never in full.
- Scan what will be committed, not what's on disk. The staged version of a file (
git show :path) is what gets pushed, and it can differ from the working copy. - Read the tracked-file list by eye. It's cheap, it's the last step, and it catches whole categories the automated passes are blind to.
- Rewrite history for anything found in it. Use
git filter-repo --replace-text, then verify the value appears zero times in the rewritten history. - Rotate anything that was ever exposed. Scrubbing a repository doesn't reach backups, sync targets or logs that already hold a copy.
Keeping it clean afterwards
- Secrets live outside the code. Each service's credentials sit in a file only the owner can read (mode 0600), and scripts read them at runtime. Nothing is hardcoded.
- Allowlist, not denylist. The repository tracks only its
scripts/andsrc/directories. Everything else is ignored by default, so a new data file or config dump can't be committed by accident. - A scan before every push. gitleaks runs over staged content before each push, and the public repository is treated as published the moment anything is pushed.
- A guard on the assistant too. Claude, the AI coding assistant I build with, writes files on my behalf, so its writes pass through a hook that blocks secret-shaped strings from being written at all. The scans described here were run together with it.
Scan output is a leak path too
A secret scanner that prints what it finds copies the secret into terminal scrollback, CI logs and session transcripts, which are often more widely synced than the code itself. Three rules:
- Print counts, positions or hashes, never matched values.
- Test a masking pattern on a known sample before trusting it. It must cover every syntactic form in the codebase: quotes, function arguments, URLs, form fills. A mask that handles one form and misses another prints the secret in full.
- For API objects, print an allowlist of fields. Printing "everything except the secret field" is not safe. Status and settings endpoints routinely return webhook URLs, keys and passwords under names you didn't anticipate.
Takeaways
- A clean pattern scan is necessary, not sufficient. If you have the secret, search for the secret.
- History is part of the repository. So are backups of the repository.
- Treat your own tooling's output as a place secrets can land.