Find the secrets you typed into your terminal, and redact them before they leak.
Every export OPENAI_API_KEY=..., mysql -pSecret, curl -H "Authorization: Bearer ..."
and git clone https://user:token@... you ever typed is still sitting in a plain-text
history file in your home folder, readable by every process you run and copied into
every backup and dotfile sync. Histclean finds those secrets across shell and REPL
history files, shows each one masked, and on request replaces exactly the secret bytes
with <redacted:detector-id> (or removes the whole entry), leaving every other byte of
the file untouched.
It is a native macOS app and a zero-dependency command-line tool built from one Swift package. Histclean is completely offline: it has no networking code, never uploads, checks or validates a secret against any service, and collects no telemetry. Scanning is read-only; nothing is written until you confirm a redaction.
Histclean is open source and signed with an ad-hoc signature, not an Apple Developer ID. Pick the install path that suits you.
1. Homebrew, built from source (no Gatekeeper prompt)
brew install mk24x7/tap/histclean
histclean # the CLI is on your PATH
ln -sfn "$(brew --prefix histclean)/Histclean.app" /Applications/Histclean.appThe CLI and the app are compiled on your Mac, so they carry no quarantine flag and open without any security dialog. Needs Xcode 15 or later.
2. Direct download
Download Histclean-<version>-macos-universal.dmg or .zip (the app) or
histclean-<version>-macos-universal.tar.gz (the CLI) from the
latest release, verify it against
SHA256SUMS.txt, and copy Histclean.app to /Applications or histclean to a folder
on your PATH. Then either:
- open it once, click Done in the "Apple could not verify" dialog, go to System Settings > Privacy & Security, scroll to Security and click Open Anyway (macOS 15 and 26 no longer offer the Control-click shortcut), or
- clear the quarantine flag from Terminal:
xattr -d com.apple.quarantine /Applications/Histclean.app
xattr -d com.apple.quarantine /usr/local/bin/histclean3. Homebrew cask (prebuilt app, quarantined)
brew install --cask mk24x7/tap/histclean-appHomebrew no longer strips quarantine, so this path shows the same first-launch dialog
as the direct download. Use it if you manage your Mac with brew bundle.
4. Build from source
git clone https://github.com/mk24x7/histclean.git && cd histclean
./build.sh # dist/Histclean.app and dist/histclean, ad-hoc signed
./package.sh # optional: zip, dmg, CLI tarball and checksumshistclean # scan every known history file, read-only
histclean --redact # asks once per file, then redacts
histclean --redact --backup # keep a 0600 copy in ~/.histclean/backups first
cat build.log | histclean --stdin --min-severity high # CI: exit 3 on findings| Source | Path | Format |
|---|---|---|
| zsh | ~/.zsh_history |
plain and extended (: <start>:<elapsed>;<command>), continuations, meta-escaped bytes |
| bash | ~/.bash_history |
one command per line, optional #<epoch> lines |
| fish | ~/.local/share/fish/fish_history (honours XDG_DATA_HOME) |
- cmd: records with when: and paths: |
| Python REPL | ~/.python_history |
lines |
| Node.js REPL | ~/.node_repl_history |
lines |
| MySQL client | ~/.mysql_history |
lines |
| psql | ~/.psql_history |
lines |
| Ruby irb | ~/.irb_history |
lines |
| SQLite shell | ~/.sqlite_history |
lines |
| redis-cli | ~/.rediscli_history |
lines |
| less | ~/.lesshst |
lines |
$HISTFILE |
wherever it points, when set | detected from name and content |
Any other file can be passed on the command line or added in the app, and --stdin
scans piped text (build logs, CI output).
Specific detectors run first; when two detectors match the same bytes the earlier one
wins, so an export GITHUB_TOKEN=ghp_... is reported once, as a GitHub token.
| Detector | What it finds | Severity |
|---|---|---|
private-key |
-----BEGIN ... PRIVATE KEY----- blocks |
high |
aws-access-key |
AWS access key IDs (AKIA..., ASIA...) |
high |
aws-secret-key |
40-character values of aws_secret_access_key and similar |
high |
github-token |
ghp_, gho_, ghu_, ghs_, ghr_ tokens |
high |
github-pat |
Fine-grained github_pat_ tokens |
high |
gitlab-token |
glpat- tokens |
high |
slack-token |
xoxa-, xoxb-, xoxp-, xoxr- tokens |
high |
stripe-live-key |
sk_live_ and rk_live_ keys |
high |
stripe-test-key |
sk_test_ keys |
low |
anthropic-key |
sk-ant- keys |
high |
openai-key |
sk-, sk-proj-, sk-svcacct- keys (must mix case and digits) |
high |
google-api-key |
AIza keys |
high |
npm-token |
npm_ tokens |
high |
pypi-token |
pypi- tokens |
high |
huggingface-token |
hf_ tokens |
high |
vercel-token |
VERCEL_TOKEN=... and vercel --token ... |
high |
cloudflare-token |
CLOUDFLARE_API_TOKEN, CF_API_KEY, X-Auth-Key |
high |
digitalocean-token |
dop_v1_, doo_v1_, dor_v1_ tokens |
high |
telegram-bot-token |
<bot id>:AA... Bot API tokens |
high |
sendgrid-key |
SG.<id>.<secret> keys |
high |
twilio-key |
SK plus 32 hex characters |
high |
jwt |
JSON Web Tokens (eyJ... header, payload, signature) |
medium |
auth-bearer |
Authorization: Bearer ... |
high |
auth-basic |
Authorization: Basic ... (must decode to user:password), curl -u user:password |
high |
url-credentials |
scheme://user:password@host |
high |
mysql-password |
mysql -p<password>, MYSQL_PWD= |
high |
postgres-password |
PGPASSWORD= |
high |
password-flag |
--password=<value>, --password <value> |
high |
export-secret |
export NAME=value (and fish set -x) where NAME contains SECRET, TOKEN, KEY or PASSWORD |
medium |
generic-assignment |
password, secret, token, api_key, access_key, auth followed by = or : and 8+ characters |
medium |
high-entropy |
24+ character token with Shannon entropy above 4.0 bits per character | low |
The generic, export and entropy detectors skip values that are clearly not secrets:
- git object ids (40 or 64 hex) in
gitcommands, and content digests such assha256:<hex>orsha512-<base64> - UUIDs
- base64 that decodes to ordinary printable text (for example file content in
echo) - file paths (
/...,~/...,./...,@file, names with a file extension) and URLs that carry no credentials - shell references:
$VAR,${VAR},$(command)and backticks - placeholders:
<token>,{{ token }},xxxx,****,your_api_key, and values Histclean already redacted
Provider detectors with a fixed prefix and length (GitHub, AWS, Stripe and so on) do not
apply the placeholder rule: ghp_ followed by 36 characters is reported even if the
characters are all x, because the prefix and length alone are a strong signal. Hex
strings can never exceed 4.0 bits per character, so the entropy detector never reports
hashes.
Missing a provider or seeing a false positive? Open a detector request with a made-up example. If you found a real secret Histclean misses, do not paste it anywhere: see SECURITY.md.
- Re-check. The file is read and scanned again. Every selected finding must still be at the same byte range with the same detector; otherwise nothing is written and you are asked to scan again.
- Rewrite in memory. Only the secret bytes change, each becoming
<redacted:detector-id>. zsh timestamps, meta escapes and continuation lines, bash#<epoch>lines and fishwhen:/paths:fields stay exactly as they were. With--delete-lines(or "Delete selected lines") the whole entry is removed instead, including its timestamp line and continuation lines. - Optional backup.
--backupcopies the original to~/.histclean/backups/<name>.<timestamp>with mode 0600 in a 0700 directory. The backup still contains the secrets; delete it once you have checked the result. - Atomic replace. The new content is written to a temporary file in the same
directory, flushed to disk, given the original permissions (0600 stays 0600) and
owner, and moved over the original with
rename(2). If another process changed the file in the meantime, the attempt is repeated once and then abandoned. Symbolic links are followed, so a history file linked from a dotfiles repository stays linked. - Verify and log. The file is read back and compared byte for byte with the
intended content. Each redaction is appended to
~/Library/Logs/Histclean/redactions.jsonlwith the file, line, detector, action and time. The log never contains the secret, a preview or the command.
- Running shells keep history in memory. An open zsh, bash or fish session writes its
in-memory history back when it exits, which can restore a secret you just redacted.
Close other terminal sessions first, or run this in each one after redacting:
- zsh:
fc -R; fc -W - bash:
history -c; history -r - fish:
history --merge
- zsh:
- Copies elsewhere are untouched. Time Machine snapshots and backups, iCloud Drive or
other synced dotfiles, dotfiles repositories, terminal scrollback and logs, and
Histclean's own
--backupcopies all still hold the old content. - Rotate what matters. Redaction reduces exposure from now on; it cannot tell you whether the secret was already copied. Treat anything with high severity as compromised and rotate it.
- Detection is pattern based. A password typed as a bare word with no recognisable context cannot be told apart from any other argument.
histclean [file ...] [options]
Input:
file ... Scan these files instead of the known sources
--stdin Scan text read from standard input
--all-sources Also scan every known source when files are given
Filtering and output:
--min-severity <level> low (default), medium or high
--json One JSON document on stdout
--no-color Disable colors (NO_COLOR is also honoured)
Changing files:
--redact Replace each secret with <redacted:detector-id>
--delete-lines Remove every entry that contains a secret
--backup Copy each file to ~/.histclean/backups first (0600;
the copy still contains the secrets)
-y, --yes Do not ask for confirmation per file
-V, --version Show the version
-h, --help Show this help
--redact asks once per file and lists the counts per detector; --yes skips the
question and is required when standard input is not a terminal. Standard input cannot be
redacted in place. HISTCLEAN_LOG_DIR and HISTCLEAN_BACKUP_DIR move the log and the
backups.
The JSON document has version, sources (path, format, entries, findings), findings
(file, line, timestamp, detector, detectorName, severity, masked preview, byteStart,
byteEnd), summary (total, byDetector, bySeverity), errors, redactions (with
--redact or --delete-lines) and warning.
| Code | Meaning |
|---|---|
| 0 | No findings, or every selected finding was redacted |
| 1 | Runtime error, for example a named file does not exist or cannot be read |
| 2 | Usage error |
| 3 | Findings remain: scan mode found secrets, or a redaction was declined or failed |
Does Histclean send anything anywhere? No. There is no networking code in the app or the CLI, no update check, no analytics and no secret validation against provider APIs. You can verify with Little Snitch or by reading the source; the only files it writes are the history files you confirm, the optional backups and the redaction log.
Why is the preview masked? Previews show the first 4 and last 2 characters of a secret (only the first 2 for secrets under 12 characters, none under 8), so you can recognise it without the tool printing it in full into your terminal scrollback.
Will it break my history file? Redaction replaces only the secret bytes and is
checked by reading the file back. The test suite round-trips zsh files with extended
timestamps, continuation lines and meta-escaped multibyte characters byte for byte. If
you want extra certainty, use --backup for the first run.
Why did my secret come back? A shell that was open during the redaction wrote its in-memory history back on exit. See the limits above.
Why the Gatekeeper dialog? Histclean is not notarized because it is a free, open-source side project. The Homebrew formula avoids the dialog entirely by compiling on your machine.
Does it need Full Disk Access? No. History files live directly in your home folder, which needs no extra permission.
See CONTRIBUTING.md. Adding a detector is one entry in
Sources/HistcleanCore/Detectors.swift, positive and negative lines in the test corpus,
and a row in the table above. Security issues and detector gaps involving real secrets:
SECURITY.md.
