Remove secrets and files from history¶
Scenario¶
Three weeks ago someone committed config/secrets.env with a live API key in it.
The file was deleted a few commits later — but deletion only removes it from the
tip; every clone still carries the key in history. In the same repository a
build/ directory was accidentally tracked for a while, bloating the repo.
regit offers two branch-scoped, preview-first tools for this:
purgeremoves whole files or directories from branch history as if they were never added.redactfinds a literal secret string across branch history and removes the matching lines (or replaces just the secret within them), following the line forward through later rotations and renames.
A rewrite is not revocation — rotate the credential regardless
Both verbs rewrite only the current branch. Other branches, tags, the
reflog, and regit's own backup refs still reference the old objects until git
prunes them (git gc) — and anyone who already pulled has the secret anyway.
The secret value also lands in .git/regit/command.log and your shell history
through the very invocation that removes it. Treat the key as compromised and
rotate it, whatever the rewrite says. For whole-repo, all-refs scrubbing
use git filter-repo; regit is the branch-scoped, preview-first, undoable
member of that family.
Prerequisites¶
- A clean working tree
- The exact secret string, or the repo-relative path(s) to remove —
purgetakes no globs, so use--searchfirst to find the paths
Complete example¶
# 1. Find candidate paths anywhere in history (read-only)
regit purge --search=secret
# 2. Purge the file — preview, then apply
regit purge config/secrets.env
regit purge config/secrets.env --execute
# 3. Redact the leaked key everywhere it appears — preview, then apply
regit redact --match=sk-live-abc123
regit redact --match=sk-live-abc123 --execute
Walkthrough¶
Step 1: Search history for the paths¶
--search is read-only: it lists every path in branch history matching the query
and exits — nothing is rewritten.
Renamed ancestors are separate paths; search for them too and purge all spellings.
Step 2: Purge the file¶
Purging 1 path(s):
config/secrets.env
1 affected commit(s):
34409b9d Add deployment config. ✖ will be dropped
(dry run — nothing applied; pass --execute to apply)
Commits the purge would empty are dropped from the chain (commits that were
already empty are preserved). Re-run with --execute to apply. Multiple paths can
be given at once — regit purge build/ old.log --execute — but globs like
'*.log' are rejected: pass exact repo-relative paths from --search. The
working tree is never touched; a purged path still on disk simply becomes
untracked.
Step 3: Redact the key itself¶
If the secret also appears inside files you want to keep, use redact:
Redacting "sk-live-abc123" (remove the matched line) — 1 occurrence(s):
config/secrets.env @ 34409b9d
> API_KEY=sk-live-abc123
1 affected commit(s):
34409b9d Add deployment config. rewritten
(dry run — nothing applied; pass --execute to apply)
--match is a literal substring (git's pickaxe scan), not a regex — writing
--match='sk-.*' would search for those exact characters. In the default
remove mode the whole matching line is deleted. redact also follows each hit's
line forward through later history — rotations of the value, file renames — so
descendants of the leaked line are caught too; pass --no-cascade=true to
disable that.
Step 4: Verify and clean up¶
After --execute, the standard apply summary reports the new HEAD and backup
ref. regit undo restores the pre-rewrite state if the result looks wrong —
which also restores the secret, so decide before force-pushing.
Expected output¶
Both verbs preview by default and exit 0; --execute applies and exits 0 on
success. Exit 1 is a usage error (e.g. a glob passed to purge, or
--mode=replace without --replace-pattern), exit 2 a blocked guard (dirty
tree, a merge commit in the rewritten range, pushed history without --force),
exit 3 an apply error.
Variations¶
Replace the secret instead of deleting the line¶
Keep the line, swap only the key for a placeholder — --replace-pattern is an
RE2 regex applied within each matched line, with $1 capture refs available in
--replace-with:
regit redact --match=sk-live-abc123 --mode=replace --replace-pattern='sk-live-\S+' --replace-with=REDACTED --execute
Narrow the scan to certain paths¶
--in takes glob(s) limiting which paths are scanned (unlike purge, which
takes exact paths only).
Merge commits and side branches¶
Both verbs block (exit 2) if a merge commit sits in the rewritten range, and
purge also blocks when the path has history on a merged side branch. redact
reports secrets found in merged side-branch history as an advisory note — it
rewrites only the first-parent chain, never that side branch's own commits.