How to strip api keys and secrets from markdown
- Step 1Open the Secret Redactor — Go to /markdown-tools/md-secret-redactor. It accepts a single Markdown document — paste text or drop one
.mdfile (acceptsMultipleis false, so no batch). - Step 2Decide on scope before running — Leave
scanAlloff to redact inside fenced ``code blocks only — the right default when secrets live in example snippets. TurnscanAllon if credentials might appear in prose, inlinebacktick` spans, or headings. - Step 3Run the redaction — The five patterns apply in order: AWS key, keyword assignment,
Bearer, JWT, then PEM block. Matches become[REDACTED],[REDACTED_AWS_KEY],[REDACTED_JWT], or[REDACTED_PRIVATE_KEY]. - Step 4Read the output for placeholders — Scan the result for the placeholder strings. Their presence confirms a match fired; their absence on a line you expected to be scrubbed means the value did not match any of the five patterns.
- Step 5Manually check the formats it cannot catch — Bare
sk-.../ghp_...keys, Slackxoxb-tokens, Stripesk_live_keys, base64 secrets, and 40-char AWS secret access keys are NOT matched on their own. Search the doc for those yourself. - Step 6Rotate anything that leaked, then publish — Redacting the doc does not un-leak a credential that was already committed. Treat any real key you find as compromised, rotate it, then publish the redacted Markdown.
What the redactor actually detects
The five regex patterns the redactor applies, in the exact order it applies them, taken from lib/markdown/markdown-engine.ts. There are no other patterns — anything not matched here is left untouched.
| Pattern (what it matches) | Example that matches | Replaced with | Order |
|---|---|---|---|
AWS access key id: AKIA + 16 uppercase letters/digits (case-sensitive) | AKIAIOSFODNN7EXAMPLE | [REDACTED_AWS_KEY] | 1 |
Keyword assignment: api_key, api-key, apikey, token, secret, password, passwd, pwd, authorization followed by =/:/space, then an 8+ char value | api_key = abcd12345678 | <keyword>=[REDACTED] (separator normalized to =) | 2 |
Bearer + an 8+ char token | Bearer eyJhbGci... | Bearer [REDACTED] | 3 |
Three-segment JWT: eyJ + 10+ chars, dot, 10+ chars, dot, 10+ chars | eyJhbGci....eyJzdWIi....SflKxw... | [REDACTED_JWT] | 4 |
PEM private-key block: -----BEGIN ... KEY----- ... -----END ... KEY----- | an RSA/EC/OPENSSH key block | [REDACTED_PRIVATE_KEY] | 5 |
Scope: which parts of the document are scanned
The single scanAll option controls scope. Default (off) restricts redaction to fenced `` code blocks only; on scans the entire document. Inline backtick` spans and 4-space indented code are treated as prose, not code blocks.
| Document region | scanAll: false (default) | scanAll: true |
|---|---|---|
| Fenced ``` code block | Scanned | Scanned |
| Prose / paragraph text | Left untouched | Scanned |
Inline backtick code span | Left untouched (counts as prose) | Scanned |
| 4-space indented code block | Left untouched (only ``` fences count) | Scanned |
| Headings, tables, blockquotes | Left untouched | Scanned |
The redactor's option contract
The Secret Redactor exposes exactly one control. There are no presets, no per-pattern toggles, no custom-pattern field, and no allow-list — the schema in lib/markdown/markdown-tool-schemas.ts defines only this.
| Option | Type | Default | What it does |
|---|---|---|---|
scanAll | boolean | false | Off: redact inside fenced code blocks only. On: redact prose, inline code, and headings too. |
| (input) | single .md file or pasted text | — | inputType is markdown; acceptsMultiple is false, so one document at a time (no batch). |
| (output) | markdown (.md), same filename | — | outputType is markdown; you get the same document back with matches replaced by placeholders. |
Cookbook
Real before/after runs against the actual engine. The placeholder text and the separator-normalization quirk are exactly what the code produces.
Keyword assignment inside a fenced block
The bread-and-butter case. With scanAll off, the redactor scans the fenced block, matches the api_key keyword followed by an 8+ char value, and normalizes the separator to =.
Input: ```bash api_key = sk-abcd1234efgh5678ijkl export REGION=us-east-1 ``` Output (scanAll: false): ```bash api_key=[REDACTED] export REGION=us-east-1 ```
A bare OpenAI key is NOT redacted
There is no sk- pattern in the engine. A standalone OpenAI-style key on its own line passes through untouched. It is only caught if a keyword like token= or api_key= precedes it.
Input: ```text sk-proj-aBcD1234aBcD1234aBcD1234aBcD1234 token=sk-proj-aBcD1234aBcD1234aBcD1234aBcD1234 ``` Output: ```text sk-proj-aBcD1234aBcD1234aBcD1234aBcD1234 token=[REDACTED] ```
AWS key id vs. AWS secret key
The AKIA + 16 uppercase pattern catches access-key IDs only and is case-sensitive. A 40-char secret access key is indistinguishable from random base64, so the redactor leaves it alone unless a keyword precedes it.
Input: ```ini aws_access_key_id = AKIAIOSFODNN7EXAMPLE aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY ``` Output: ```ini aws_access_key_id = [REDACTED_AWS_KEY] aws_secret_access_key=[REDACTED] ``` (the secret was caught only because `aws_secret_access_key` contains the keyword `secret`)
Default scope leaves prose keys alone
With scanAll off, a secret pasted into a paragraph survives because only fenced blocks are scanned. Turn scanAll on to scrub prose and inline code too.
Input: The key is `AKIAIOSFODNN7EXAMPLE` — see below. ``` AKIAIOSFODNN7EXAMPLE ``` scanAll: false → The key is `AKIAIOSFODNN7EXAMPLE` — see below. ``` [REDACTED_AWS_KEY] ``` scanAll: true → The key is `[REDACTED_AWS_KEY]` — see below. ``` [REDACTED_AWS_KEY] ```
The dangling-quote quirk
The keyword pattern consumes an optional opening quote but not the closing one, so a quoted value leaves a stray quote behind. Cosmetic, but worth knowing before you commit the redacted file.
Input: ```yaml password: "hunter2pass" ``` Output: ```yaml password=[REDACTED]" ``` (note the trailing " and the : changed to =)
Edge cases and what actually happens
Bare `sk-...` / `ghp_...` / `xoxb-...` / `sk_live_...` key on its own line
Not detectedThere is no pattern for OpenAI, GitHub, Slack, or Stripe key prefixes. They are only redacted when a keyword like token= or api_key= precedes them. Search for these prefixes manually.
AWS secret access key (40-char base64)
Not detectedOnly the AKIA access-key ID is matched. A 40-char secret key matches no pattern on its own and is caught only if a keyword such as secret/password precedes it.
Lowercase `akia...`
Not detectedThe AWS pattern is case-sensitive (AKIA[0-9A-Z]{16}). Lowercase or mixed-case variants are ignored.
Secret in prose with scanAll off
PreservedBy design, default scope is fenced code blocks only. A credential in a paragraph or inline backtick span survives until you enable scanAll.
Quoted value leaves a dangling quote
By designThe keyword pattern strips an opening quote but not the closing one, and rewrites the separator to =, so password: "x" becomes password=[REDACTED]".
JWT split across two lines
Not detectedThe JWT pattern requires eyJ... three dotted segments on one line. A wrapped or hard-broken token does not match.
Short value (under 8 chars) after a keyword
PreservedThe keyword pattern needs an 8+ char value. pwd=abc is left as-is — assumed to be a placeholder, not a real secret.
False positive on a long non-secret after a keyword
Expectedtoken = your-token-here-please matches the keyword pattern and gets redacted even though it is a placeholder. Spot-check the output for over-redaction.
Document over the tier char/byte limit
RejectedFree tier caps at 1 MB / 500,000 characters. A larger doc is rejected before processing; upgrade or split it with md-splitter.
Git history still contains the secret
Out of scopeThis tool only rewrites the current document text. A key already committed lives in history — use BFG or git-filter-repo, and rotate the key.
Frequently asked questions
What exactly does it detect?
Five patterns: AWS AKIA + 16 uppercase chars; keyword assignments (api_key/token/secret/password/passwd/pwd/authorization + an 8+ char value); Bearer tokens; three-segment eyJ... JWTs; and PEM -----BEGIN ... KEY----- blocks. Nothing else.
Does it catch a bare OpenAI sk- key or GitHub ghp_ token?
No. There is no dedicated pattern for those prefixes. They are only redacted when a keyword like token= or api_key= immediately precedes them.
What is the scanAll option?
A single boolean. Off (default) scans fenced `` code blocks only. On scans prose, inline backtick` code, and headings as well.
Are inline backtick spans scanned by default?
No. Inline code counts as prose. Only fenced ``` blocks are scanned unless you enable scanAll.
What do the placeholders look like?
Keyword/Bearer matches become [REDACTED], AWS keys become [REDACTED_AWS_KEY], JWTs become [REDACTED_JWT], and private-key blocks become [REDACTED_PRIVATE_KEY].
Why did `password: "x"` turn into `password=[REDACTED]"`?
The pattern normalizes the separator to = and consumes the opening quote but not the closing one, leaving a stray quote. It is cosmetic — fix it by hand if it bothers you.
Can I add my own patterns?
No. The schema exposes only the scanAll toggle — there is no custom-pattern field, preset, or per-pattern switch.
Can it over-redact real content?
Yes. A long placeholder after a keyword (token = your-token-here) is redacted because it matches the pattern. Spot-check the output.
Is the document uploaded anywhere?
No. Redaction runs in your browser. The Markdown never reaches a server, so you can safely process docs that contain live credentials.
Does it process multiple files at once?
No. acceptsMultiple is false — one document per run. Batch limits in the tier table apply to other markdown tools, not this one.
Should this be my only secret check?
No. Use it as one layer and add a real scanner like gitleaks or trufflehog in CI for the prefixes and entropy checks this tool does not cover.
Where do I go for related cleanup?
Strip emoji with md-emoji-remover, lint structure with md-lint, and tidy fenced blocks with md-code-block-tagger before publishing.
Privacy first
All Markdown processing runs locally in your browser using JavaScript. No file is ever uploaded to JAD Apps servers — only metadata counters are saved for signed-in dashboard stats.