How to Remove Secrets and Personal Data from Logs Before Sharing

A practical guide to redacting API keys, tokens, emails, card numbers and IPs from logs before you paste them into a ticket, chat or AI assistant.

Sooner or later every developer pastes a log somewhere it was never meant to go: a GitHub issue, a Slack channel, a vendor's support form, a Stack Overflow question, an AI chat. Logs are the most useful thing you can share when asking for help, and also the most likely to contain a customer's email address, a session token or the production database password.

This guide covers what to look for, how to redact it without making the log useless, and why "I'll just skim it first" is not enough. The sanitizer below does the mechanical part in your browser; the rest of the article explains what it can and cannot catch.

Data SanitizerPaste a log, a support ticket or a prompt and every email address, phone number, card number, IP address and API key in it is replaced before you share it. Optional on-device AI also catches names and street addresses.

What actually leaks

Sensitive values in logs fall into two groups, and they need different handling.

Credentials are the urgent ones. A leaked API key or token gives someone else your access, often within minutes: automated scanners watch public GitHub commits and paste sites for exactly these strings. The usual suspects are:

  • Authorization: Bearer ... headers and session cookies
  • JWTs, which start with eyJ and are easy to spot once you know
  • Cloud and SaaS keys with published prefixes: AKIA (AWS), ghp_ and github_pat_ (GitHub), sk_live_ (Stripe), xoxb- (Slack), AIza (Google)
  • password=, client_secret= and DB_PASSWORD= lines from a config dump or an environment printout
  • Private key blocks starting with -----BEGIN ... PRIVATE KEY-----

Personal data is the quieter problem. It rarely gets you hacked, but it can break privacy law, a customer contract or simply a user's trust. In logs it usually means email addresses, phone numbers, IP addresses, card numbers, IBANs, names and street addresses.

Why skimming does not work

A log of a few hundred lines is too long to read carefully, and the risky lines look boring. A bearer token sits on a DEBUG line between two timestamps; a password is the fifth key in a config object printed on one line. People reliably find the obvious email at the top and miss the token in the middle.

Find-and-replace has the opposite problem. Replacing one email by hand works until the same address appears in a URL-encoded query string, or in capitals, or in a second file.

Pattern-based redaction is better at the repetitive part. Your judgment is still needed for the rest.

A worked example

Here is a short, realistic application log:

2026-10-20T09:14:02Z INFO login ok [email protected] ip=203.0.113.42
2026-10-20T09:14:05Z ERROR payment failed for [email protected] card=4111 1111 1111 1111
2026-10-20T09:14:05Z DEBUG Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSJ9.c2lnbmF0dXJl
2026-10-20T09:14:06Z DEBUG config DB_PASSWORD=hunter2-prod max_tokens=1000
2026-10-20T09:14:07Z WARN retry from 203.0.113.42 for [email protected]

Run through the sanitizer with numbered tags, it becomes:

2026-10-20T09:14:02Z INFO login ok user=[EMAIL_1] ip=[IP_1]
2026-10-20T09:14:05Z ERROR payment failed for [EMAIL_1] card=[CARD_1]
2026-10-20T09:14:05Z DEBUG Authorization: Bearer [SECRET_1]
2026-10-20T09:14:06Z DEBUG config DB_PASSWORD=[SECRET_2] max_tokens=1000
2026-10-20T09:14:07Z WARN retry from [IP_1] for [EMAIL_2]

A few details make this output more useful than a blanket ***:

  • The same value keeps the same tag. Alice is [EMAIL_1] on both lines and the IP is [IP_1] both times, so a reader can still see that the retry came from the same address as the login. That relationship is often the clue that solves the bug.
  • Only the credential is replaced. Bearer and DB_PASSWORD= stay, so the reader knows what kind of value was there.
  • Configuration that only looks like a secret is left alone. max_tokens=1000 contains the word "token", but a short number is a setting, not a credential.
  • Timestamps survive. They contain grouped digits that a careless phone-number pattern would eat, and losing them would make the log useless.

Choosing a replacement style

The tool offers three styles. Pick the one that fits the reader.

StyleOutputBest for
Numbered tags[EMAIL_1], [EMAIL_2]Debugging, where it matters which values are the same
Tags[EMAIL]Documentation and examples, where identity does not matter
Asterisks*****************Fixed-width output, tables and screenshots, where alignment matters

Numbered tags are usually the right default for logs. Asterisks keep the length of the original, which is handy for column-aligned output but tells a careful reader how long the value was.

Checksums cut down false alarms

Long digit strings are everywhere in logs: order numbers, trace IDs, Unix timestamps in milliseconds. A redactor that blanks every 16-digit number makes the log unreadable, so good detection checks the number's structure as well as its shape.

  • Card numbers carry a Luhn check digit. 4111 1111 1111 1111 passes and is redacted; change the last digit to 4111 1111 1111 1112 and it fails, so it is left alone as just a number.
  • IBANs carry a mod-97 check, so GB82 WEST 1234 5698 7654 32 is recognised while a random code of the same length is not.
  • IP addresses are checked octet by octet, so a version string like 1.2.3.400 is not mistaken for one.

This is also why a sanitizer is more trustworthy than a single regex you wrote in a hurry.

What patterns cannot catch

Some sensitive data has no fixed shape. "Alice Smith", "42 Harbour Road" and a passport number all look like ordinary words and numbers to a pattern. The sanitizer handles these with an optional on-device AI model that recognises names, street addresses and ID numbers. It downloads about 30 MB once and then runs in your browser like everything else; the text still never leaves your device. Values it finds are marked so you can check them, because a model is a guess, not a rule.

Even with both layers, some things will get through:

  • Context that identifies someone. "The only customer in Reykjavik on the enterprise plan" names nobody and identifies one person.
  • Internal hostnames and paths. db-prod-eu-3.internal is not personal data, but it tells an outsider about your infrastructure.
  • Secrets without a prefix. A random 32-character string with no telltale key name looks like any other ID.
  • Encoded data. A Base64 or URL-encoded blob can contain an email address that no pattern sees.

Hashing is not anonymising

A common shortcut is to replace each email with its SHA-256 hash, so values stay consistent without being readable. It is weaker than it looks. Email addresses, phone numbers and IPv4 addresses come from small, guessable sets, and anyone can hash a list of candidates and compare. A hash of [email protected] is effectively a pseudonym that can be reversed, not anonymisation. Numbered placeholders are both safer and easier to read.

Prevent it at the source

Redacting before sharing is the last line of defence. Fewer secrets in logs in the first place is better:

  1. Never log full request headers or bodies by default. Log the fields you need, by name.
  2. Use your logging library's redaction support. Most structured loggers can mask fields such as password, authorization and cookie by key before anything is written.
  3. Log identifiers, not identities. A user ID such as u_8f3a is enough to debug with and means nothing outside your system.
  4. Keep DEBUG output out of production. That is where tokens and config dumps usually live.
  5. Add secret scanning to CI. GitHub's push protection and tools like gitleaks catch keys before they reach a public repository.

A checklist before you paste a log

  1. Cut the log to the lines that show the problem; less text means less to leak.
  2. Run it through a sanitizer and read what it replaced.
  3. Read the result once more for names, hostnames and anything encoded.
  4. If a real credential was ever exposed, rotate it, whether or not you think anyone saw it.

If the log contains a JWT and you need to show what was in it, decode it first, share only the claims that matter, and redact the token itself.

JWT DecoderPaste a JWT to read its header and payload and see when it expires. Decoding happens entirely in your browser, so the token never leaves your device.

Try the tools