npx wardlatch@latest --incident committed-keys-2026-10
Wardlatch · Committed keys · Check and fix · Free
Keys in codeAre there keys committed to your code?
If a cloud key, private key or token sits in a file your repository tracks, everyone with a copy of the code has the key: keys in code are copied wherever the code goes, and deleting the file doesn’t take them back. This month’s reminder: PingCAP, the company behind TiDB Cloud, confirmed that stolen service-account credentials gave an attacker access to its internal systems from 2026-09-09 to 2026-10-07. Find the keys in your own repositories, and revoke each real one at its provider before you remove it from the code.
- A real keyit works, or mightRevoke, then remove
- An example or test valuedocumentation, a fixtureMark the line
Search the files your repositories track.
Run Wardlatch from your home folder (after cd ~) on the machine that holds your clones; it needs Node.js 18.17 or later.
Update, 2026-11-02 (0.1.12). More key forms that LibreSSL loads are now found, or UNKNOWN where they can’t be read: unusual lengths and tags, and a version number written in an unusual way. If an earlier version said SAFE for this check, run it again. The changelog has every change.
Update, 2026-11-02 (0.1.11). A private key written in an unusual form is now UNKNOWN rather than skipped. If an earlier version said SAFE for this check, run it again. The changelog has every change.
Update, 2026-10-27 (0.1.10). If Wardlatch 0.1.1 to 0.1.9 said SAFE for this check, run it again: a core.worktree setting in a repository’s own config, pointing above the repository, made those versions read none of its files and call it safe. 0.1.10 reads each repository from its own top folder, and reads UNKNOWN where a commondir file in .git, or an empty .git folder inside another repository, makes git read another repository. It still follows a .git file to whatever repository it names (section D) (GHSA-96mq-627m-9qhg; what changed).
It finds the git repositories in the usual project folders (~/Development, ~/code, ~/src, ~/projects and the like, or the folders you give with --paths) and scans the files each one tracks for AWS access keys, private keys, Google service-account key files, and GitHub, npm, Slack and Stripe live tokens. Each finding is a REVIEW line with the key’s type, the file and the line, and never the key itself: for a token or an access key, only its four leading characters and its length. It skips lines marked secrets-ok, gitleaks:allow or pragma: allowlist secret, and a key with a placeholder word right next to it (such as example, fake or dummy, or AWS’s documented example key). A repository where a commondir file in .git, or an empty .git folder inside another repository, makes git read another repository reads UNKNOWN. It changes nothing and sends nothing.
By hand, from the repository’s top folder, the one that holds .git (anywhere else it says “Not checked”): this lists the tracked files that hold a value in one of those formats. It prints file names only, so no key lands on your screen. Like Wardlatch’s own patterns, it counts a token only where it stands on its own, not inside a longer word ((^|[^A-Za-z0-9_]) and ([^A-Za-z0-9_]|$) do what \b does in Wardlatch, in a form that git’s search accepts on macOS and Linux alike). It reads files git calls binary as text too, so an attribute can’t hide one, and LC_ALL=C keeps it from skipping a line that isn’t valid UTF-8. GIT_DIR=.git and GIT_WORK_TREE=. search this folder’s files even if a setting left in your shell, or the repository’s own core.worktree setting, points git at another repository or folder (where .git is a file, git reads the repository that file names, whichever it is: cat .git shows which), and command env -u GIT_INDEX_FILE keeps your shell from handing git another list of files, run as command env so an alias or function named env isn’t run; it keeps the repository’s settings from running a program, and downloads nothing a partial clone left out; core.quotePath=true shows a file name with a control character or a non-ASCII letter in quotes, with octal escapes (é as \303\251). It passes what git prints, its messages included, through cat -v, run as command cat so an alias or function named cat isn’t run: a control character in one of git’s messages shows in cat -v notation, such as ^[ or M-BM-^[, and isn’t acted on, and a non-ASCII letter there shows as M- sequences. It doesn’t search UTF-16 files that start with a byte-order mark, tracked files deleted from the working tree, or files marked skip-worktree that are on disk, and it reads a file marked assume-unchanged as git last stored it, not as it is on disk; Wardlatch reads all of these.
if [ -e .git ]; then command env -u GIT_INDEX_FILE LC_ALL=C GIT_DIR=.git GIT_WORK_TREE=. GIT_LITERAL_PATHSPECS=0 GIT_NO_LAZY_FETCH=1 GIT_ALLOW_PROTOCOL=none 'git' --no-pager -c core.fsmonitor=false -c core.quotePath=true grep -a -l -E '(^|[^A-Za-z0-9_])((AKIA|ASIA)[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,}|npm_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}|(sk|rk)_live_[A-Za-z0-9]{20,})([^A-Za-z0-9_]|$)|-----BEGIN ([A-Z]+ )?PRIVATE KEY|"type"[[:space:]]*:[[:space:]]*"service_account"' -- ':/' 2>&1 | LC_ALL=C command cat -v; else echo 'Not checked: run this in the folder that holds .git'; fi
- A real key
- A file that holds a key that works, or might: follow section C, starting at the provider.
- Look closer
- The search by hand also lists documentation examples and test values, which Wardlatch skips. Open the file and decide; mark a line you’ve checked with
secrets-okorgitleaks:allow(Wardlatch then skips it; the search by hand still lists the file). - Not checked
- “Not checked”, or a line that starts with
error:orfatal:, means a file or the whole repository wasn’t searched: you’re not in the repository’s top folder, a file can’t be read (Permission denied), or git couldn’t read the repository. Run it again from the top folder, as a user who can read the files. - Nothing
- No output: no value in those formats in the files this repository tracks now. The search by hand doesn’t read UTF-16 files that start with a byte-order mark, tracked files deleted from the working tree, or files marked skip-worktree that are on disk, and it reads a file marked assume-unchanged as git last stored it; Wardlatch reads all of these. Neither reads a file a sparse checkout leaves off the disk. Older commits can still hold one (section D). If
.gitis a file, check withcat .gitthat it names the repository you expect. In a.barefolder and.gitfile layout (cat .gitshowsgitdir: ./.bare), the outer folder holds no files of the repository’s own, so no output there means nothing: run it in each worktree’s folder.
There is no version to update.
A committed key isn’t a flaw in a program: the fix is at the provider that issued the key, which can revoke it and give you a new one, and in the code, which should load the key from a secret manager or the environment instead.
Revoke it, then remove it.
- Revoke or rotate the key at the provider. Deleting it from the code doesn’t un-leak it: it stays in the git history and in every clone.
- Remove it from the code and load it from a secret manager or the environment instead.
- Search the whole history with a dedicated scanner, such as gitleaks or trufflehog, and revoke every real key it finds there too.
What the check doesn’t see.
- Current files only. A key committed and later deleted is still in the history; Wardlatch doesn’t read it. Use gitleaks or trufflehog for that.
- These formats only. Passwords, database addresses with a password in them, and keys from other providers aren’t in the list.
- Files git is told to skip. The search by hand skips files marked skip-worktree and reads a file marked assume-unchanged as git last stored it; Wardlatch reads both from disk. Neither reads a file a sparse checkout leaves off the disk.
- A
.gitfile. Where.gitis a file, Wardlatch and the search by hand read the repository it names, whichever it is: one naming an empty repository reads SAFE, and the search by hand prints nothing.cat .gitshows which. Whoever can write to a repository’s.gitcan hide a file from git anyway. - A bare repository with worktrees. In a
.barefolder and.gitfile layout, the search stops at the outer folder: Wardlatch scans no worktree’s files and reads SAFE, and the search by hand prints nothing there. Pass each worktree with--paths, and run the search by hand in each worktree’s folder. - Tracked files only. Files git ignores, such as a
.env, aren’t scanned, and neither are submodules or a repository nested inside another one. - Repositories it finds. It searches four folders deep, skips folders whose names start with a dot (and
node_modules,.venv,venv), and doesn’t look inside a repository for another one; use--pathsfor clones elsewhere.--pathsreplaces the usual project folders, so name them too if they hold clones, such as--paths ~/Development --paths ~/clones. - Binary files. Wardlatch skips a file with a zero byte in its first 8,000 bytes, and a UTF-16 file that starts with a byte-order mark if it has a zero character there; the search by hand reads binary files.
- Your machine’s clones. Repositories you haven’t cloned here, and keys pasted into tickets, chats or email, aren’t seen.
This page describes Wardlatch 0.1.13 and its incident file, committed-keys-2026-10.json.
Sources and credits.
- PingCAP, security update, October 2026, 2026-10-19: compromised service-account credentials gave an unauthorised party access to certain internal systems from 2026-09-09 to 2026-10-07; PingCAP learned of it on 2026-10-07.
- Wardlatch 0.1.13, committed-keys-2026-10.json, secrets.mjs and README: the formats it looks for, what it skips, and the fix.
Credits · PingCAP disclosed its incident itself; its update doesn’t say who found it. All sources read 2026-10-25.
Debrief.
The questions this page answers, every answer open.
Why is a key committed to code a problem?
Because keys in code are copied wherever the code goes, and deleting the file doesn’t take the key back: it stays in the git history and in every clone.
What should I do when I find a committed key?
Revoke or rotate it at the provider that issued it, before anything else. Then remove it from the code and load it from a secret manager or the environment, and search the whole history with a scanner such as gitleaks or trufflehog.
How do I search my repository without printing the keys?
Use git --no-pager grep with -l, which prints the names of matching files and not the lines, and -a, so a file git calls binary is searched too, from the repository’s top folder. The check has the command, with patterns for AWS access keys, private keys, Google service-account files and GitHub, npm, Slack and Stripe live tokens.
What does the TiDB Cloud incident have to do with this?
PingCAP, the company behind TiDB Cloud, confirmed that stolen service-account credentials gave an attacker access to its internal systems from 2026-09-09 to 2026-10-07 (PingCAP’s update). It is this month’s reminder of what a compromised credential can open.
Does Wardlatch find committed keys?
In the files your local repositories track now: npx wardlatch@latest --incident committed-keys-2026-10 shows each finding’s type, file and line as a REVIEW line, never the key itself, and skips lines marked secrets-ok, gitleaks:allow or pragma: allowlist secret. It skips a file with a zero byte in its first 8,000 bytes, and a UTF-16 file that starts with a byte-order mark if it has a zero character there. It searches four folders deep for repositories, skips folders whose names start with a dot (and node_modules, .venv, venv), and doesn’t look inside a repository for another one; use --paths for clones elsewhere. --paths replaces the usual project folders, so name them too if they hold clones, such as --paths ~/Development --paths ~/clones. It doesn’t read the history: use gitleaks or trufflehog for that. Wardlatch 0.1.1 to 0.1.9 could call a repository safe here without reading its files, where a core.worktree setting in the repository’s own config pointed above it, and 0.1.10 fixes that: if one of them said SAFE here, run it again. 0.1.10 still follows a .git file to whatever repository it names: check with cat .git.