Redact secrets out of a log and keep the log readable.
Your log, with the live credentials still in it.
09:14:02 api INFO auth ok token=ghp_a1b2c3d4e5a1b2c3d4e5a1b2c3d4e5a1b2c3d4e5
09:14:03 api ERROR upstream 500 url=https://svc:hunter2@db.internal/app
09:14:09 api INFO retry with token=ghp_a1b2c3d4e5a1b2c3d4e5a1b2c3d4e5a1b2c3d4e5
09:14:11 api WARN aws AKIAIOSFODNN7EXAMPLE denied
$ redactkit app.log -m keys.json -o safe.log
The same log, still a log. The same secret gets the same tag wherever it appears; a different secret gets a different tag; the timestamps are untouched.
09:14:02 api INFO auth ok token=[GITHUB_TOKEN_1]
09:14:03 api ERROR upstream 500 url=https://svc:[PASSWORD_1]@db.internal/app
09:14:09 api INFO retry with token=[GITHUB_TOKEN_1]
09:14:11 api WARN aws [AWS_KEY_1] denied
They answer quoting a tag. You turn it back into the real thing, from a key map that never left your disk.
$ cat reply.txt
whatever [GITHUB_TOKEN_1] is, it expired yesterday
$ redactkit --restore -m keys.json < reply.txt
whatever ghp_a1b2c3d4e5a1b2c3d4e5a1b2c3d4e5a1b2c3d4e5 is, it expired yesterday
redactkit: restored 1 placeholder
The first and third lines of that log used the same credential, and you can
still see that they did. The second used a different one, and you can still see that too. That
is the difference between a redacted log and a log full of the word REDACTED: one
of them is still worth reading.
A command-line tool. It reads a pipe or a pile of files, writes a version you can paste anywhere, and — this is the part that matters — lets you turn the replies back into the real thing on your own machine. No network calls, no telemetry, no dependencies.
A secret scanner tells you there is a secret at line 42. That is a different job: it finds the leak, and then hands the log back to you exactly as it was. You still cannot share it.
sed 's/ghp_[A-Za-z0-9]*/REDACTED/g' hands you back a log, but every secret
becomes the same word. A trace with three different tokens in it collapses into noise, and
you can no longer tell which request was which — which is usually the exact thing you were
trying to show someone.
$ sed 's/ghp_[A-Za-z0-9]*/REDACTED/g'
09:14:02 api INFO auth ok token=REDACTED
09:14:03 api ERROR upstream 500 url=https://svc:hunter2@db.internal/app
09:14:09 api INFO retry with token=REDACTED
09:14:11 api WARN aws AKIAIOSFODNN7EXAMPLE denied
Both tokens became the same word, so the trace no longer tells you which
request was which. And the password and the cloud key are still sitting there in the clear,
because the pattern was never told to look for them.
$ redactkit app.log
09:14:02 api INFO auth ok token=[GITHUB_TOKEN_1]
09:14:03 api ERROR upstream 500 url=https://svc:[PASSWORD_1]@db.internal/app
09:14:09 api INFO retry with token=[GITHUB_TOKEN_1]
09:14:11 api WARN aws [AWS_KEY_1] denied
Every credential is gone, each one has a tag you can talk about, and the
repeat is still visibly a repeat.
The tags in the figures above stay stable across every file in a run, so a token in
api.log and the same token in worker.log are still recognisably one
token. And the timestamps came through untouched, which sounds like a low bar and is not: a
redactor that mangles 09:14:02 destroys the only thing that makes a log a log.
That case is one of the regression tests.
Turning the replies back into real values is the feature the browser tool cannot have, and the reason redactkit exists as a separate thing. It works because the run leaves a key map behind on your disk, and the tool is loud about what that file is.
$ redactkit app.log -m keys.json -o safe.log
redactkit: wrote keys.json (mode 600) -- it holds the real secrets, keep it local
redactkit: 4 replacements, 3 distinct values
GITHUB_TOKEN x2, AWS_KEY x1, PASSWORD x1
$ stat -c %A keys.json
-rw-------
The summary goes to standard error, so piping the tool into something else still gets you a clean log and still tells you what happened. The key map is written readable by you alone, and the tool says out loud, every single time, that the file holds the real secrets.
keys.json holds the original secrets in the clear. redactkit writes it
mode 0600 and says so on stderr every single time. It never leaves your
machine, because redactkit makes no network calls at all — there is nowhere for it to go.
So you can hand safe.log to your colleague, your vendor, your issue tracker or
your language model, let them answer in terms of the tags, and still be the only person who can
turn a tag back into a credential.
The browser tool stays free forever, and it is not a crippled demo: it is the same detectors, and it always will be, because they are generated from the same source. What you pay for is everything you cannot do by pasting into a text box.
| Browser tool | redactkit | |
|---|---|---|
| The full detector table | Yes | Yes |
| Runs with nothing uploaded | Yes | Yes |
| Numbered placeholders | Yes | Yes |
Pipe it: npm test 2>&1 | redactkit | No | Yes |
| Many files, one stable numbering | No | Yes |
| Reversible key map, kept local | No | Yes |
| Exit code for CI and pre-commit | No | Yes |
| JSON output for your own scripts | No | Yes |
| Per-project policy file | No | Yes |
| Source you can read and change | View source | ~400 lines, no deps |
For most of this project's life there was a price on this page — $29 for one developer, $99 for a team — sitting above an order form that asked you to email me so I could quote you a wallet transfer by hand. Not one person ever did. That is a measurement, not a complaint: an offer you cannot complete in a minute, from a seller you have never heard of, is not an offer. Leaving it up once two real checkouts existed elsewhere on this site was the part that was actually wrong, so it is gone.
redactkit is free now, MIT licensed, and whole — the same build the figures above were captured from, not a trimmed demo. Take it and never come back.
Download redactkit → No dependencies, Node 18 or newer. No account, no email, nothing to accept.No licence server, no activation, no phone-home — there never was one, and now there is nothing for one to check. You may read every line, change anything, vendor it into your own repo, and keep using it if this project stops existing, which it might.
If you want the engine as a dependency rather than a command,
npm install logscrub is the same detectors, generated from the same source file.
Two things, both with a checkout you can finish without ever talking to me: the scanner regression suite, which is a subscription to the upkeep of the two corpora that measure tools like this one, and the starter kit, which is the harness this whole project runs on. What I sell is maintenance and the record behind it. A one-off download was never the right thing to charge for, and pretending otherwise cost me nothing except the credibility of this page.
The browser tool takes what you paste, and a clipboard only ever holds text something
else already decoded. A command-line tool reads files, which is a harder job: a log
PowerShell wrote with > is UTF-16, a log from an older Windows or Japanese
system is in a legacy encoding, and a dump someone handed you may not be text at all. Read
every one of those as UTF-8 and the result looks like success in the worst way — either
nothing matches and the file reports clean, or the credentials are found correctly while the
log around them is quietly replaced with question marks.
So redactkit reads the bytes first and asks what they are, using the same sniffer the browser tool uses, generated from the same page.
$ xxd ps.log | head -1
00000000: fffe 3000 3900 3a00 3100 3400 3a00 3000 ..0.9.:.1.4.:.0.
That is the trap. Read as UTF-8, every character in this log is separated from the next by a zero byte, so nothing matches a credential pattern and the file reports clean while the credential sits in it in the clear.
$ redactkit ps.log
redactkit: ps.log: decoded as UTF-16 LE; output is UTF-8
redactkit: 1 replacement, 1 distinct value
GITHUB_TOKEN x1
09:14:02 deploy INFO starting release
09:14:04 deploy INFO token=[GITHUB_TOKEN_1]
$ redactkit dump.gz
redactkit: dump.gz: This is gzip, not text. A pattern scanner reads nothing useful inside it, so a clean result would be meaningless. Decompress or export it to text first.
If you know what it really is, say so: --encoding LABEL
$ echo $?
2
$ redactkit old.log
redactkit: old.log is not valid UTF-8, so it was written in a legacy encoding (11 of 64 bytes are outside ASCII).
$ redactkit --encoding windows-1251 old.log
Ошибка входа token=[GITHUB_TOKEN_1]
Refusing is the right default because this tool writes files. Reading legacy bytes as UTF-8 finds the credential correctly and destroys the log around it in the same pass, and you would only discover that after saving the result over the original. An error message costs you a minute; a lossy read costs you the log.
Read this part before you rely on it. The tool is worth more to you if you know where it stops — so here is where it stops, shown rather than promised.
auth ok token=[GITHUB_TOKEN_1]
Redacted. A shape the tool knows on sight, with no help from the text around it.db_password=[SECRET_1] host=orders-db-03.internal
Redacted. No recognisable shape at all — but the key name sitting next to it gives it away, and the hostname beside it is left alone.ticket ACME-4417 for cus_Nf8Kq2LmXpQrSt on orders-db-03.internal
Left alone, on purpose. A ticket id, a customer id and an internal hostname are not credentials. Redacting them would cost you the ability to read your own log and buy you nothing.correcthorsebatterystaple
Straight through. A password alone on a line, with no shape and nothing beside it to name it. This is pattern matching, not comprehension: nothing matching does not mean the text is clean.token=[SECRET_2]
d4e5a1b2c3d4e5a1b2c3d4e5
Worse than a miss. One credential wrapped across two lines. The head is replaced and the tail is not, so the output looks redacted while the rest of the secret is still sitting there. Check long wrapped values yourself.✓caught –left alone on purpose ✗went through, wholly or in part. Every one of these rows is re-run and re-checked whenever this page is built, so if a change to the detectors made one of them stale, the build fails instead of the page lying.
The same limits in words, because a figure is not searchable and a reader should be able to find them in the README too:
Every rule above reads the text in front of it. So a credential that arrives base64 encoded — a Kubernetes secret echoed into a deploy log, a JWT payload, a config blob — matches nothing, and the tool used to print nothing was replaced over it and exit clean. In a pre-commit hook that is not a missed warning; it is the hook letting the commit through.
redactkit now decodes each encoded run it did not already redact, re-scans it with the same rules, and tells you by line what is inside. It never rewrites the blob.
$ redactkit --check --no-review wrapped.log
redactkit: nothing was replaced; 31 of 34 rules ran. That does not mean the text is clean -- read it yourself before you share it.
not looked for: phone, uuid, hexblob
$ echo $?
0
$ redactkit --check wrapped.log
redactkit: nothing was replaced; 31 of 34 rules ran. That does not mean the text is clean -- read it yourself before you share it.
not looked for: phone, uuid, hexblob
redactkit: SECOND LOOK -- 1 thing the redactor could not act on. Read it before you share this.
line 2 a base64 run decodes to a GITHUB_TOKEN (detector: GitHub tokens)
R0lUSFVCX1RPS0VOPWdocF9lNGY1...
The blob itself was left alone on purpose: it may be a certificate or a config you need whole.
$ echo $?
1
The blob is left in the output untouched, on purpose — it may be a certificate or a config you need whole, and a tool that guesses at rewriting encoded data will eventually corrupt something you cannot get back. What changes is that you are told, by line, and that the exit code stops a commit instead of waving it through.
Only decoded runs count by default. The other thing a redactor can tell you — the
long random strings and UUIDs it decided not to touch — is behind
--review-all, because nearly every real log has a build hash or a request id in
it and gating a commit on those would make the hook useless. The precision a rule needs is
set by what happens when it fires, not by how good the rule is.
The free tool publishes the same limits, and they are pinned by tests that fail on purpose if a code change quietly makes one of them stale. That is the actual reason to trust either of them: not that I say they are careful, but that the claims are checked by something that can go red.
bin/redactkit.mjs and lib/ — the CLI and the engine, plain
ES modules, no dependencies, Node 18 or newer.lib/detectors.mjs — generated from the free browser tool, with the source
hash stamped in package.json so a stale copy is a test failure rather than a
surprise.hooks/pre-commit — scans the staged diff's added lines, and never blocks a
commit because the hook itself is misconfigured.README.md and LICENSE.txt — including the limits above,
written down where you will actually find them.