ACTIONS SECRET LEAK CHECK · RUNS IN YOUR BROWSER

Does your GitHub Actions workflow leak a secret its own masking can't catch?

GitHub Actions masks the exact, unmodified string of a declared secret everywhere it appears in a log. It does not survive two common things: transforming a secret (base64, hex, a substring) before printing produces a different string the masking doesn't recognise, and exporting a secret-derived value to $GITHUB_ENV or $GITHUB_OUTPUT doesn't register it for masking at all — a later step that echoes it prints it in plain text. Paste your workflow YAML below and this page finds both, each with the exact fix.

Nothing is uploaded. The YAML you paste is read in this browser and never leaves your device, and this tool makes no network calls of its own.

1. Paste one or more workflow files

Copy the contents of each file under .github/workflows/ you want checked. Add more blocks for more files — nothing here reads your repository, so pasting is how it sees anything.

Paste at least one workflow file above and click Check.

Prefer a CLI, or want this in your own CI?

Same logic, zero dependencies, one file:

python3 check.py                  # scan .github/workflows under the current directory
python3 check.py /path/to/repo    # scan a specific checkout
python3 check.py --format json    # machine-readable
python3 check.py --format sarif   # SARIF 2.1.0, for github/codeql-action/upload-sarif
python3 check.py --fail-on-risk   # exit 2 if any finding exists

Get check.py from the repo — stdlib only, nothing to install, no network calls. Wire --fail-on-risk into your own CI to block a newly-added finding before it merges.

Common questions

Doesn't GitHub Actions already mask secrets in logs?

Yes, for the exact, unmodified string of a declared secrets.* value (or an env: var sourced from one) — the runner replaces every verbatim occurrence of that string in log output, including inside set -x trace output. This tool does not flag that case. It flags the two documented ways a value still reaches a log unmasked: transforming the secret first, and exporting a secret-derived value without registering it for masking.

Why does transforming a secret bypass masking?

GitHub's masking is an exact string match against the known secret value. base64, hex, case-folding, a substring, or any other re-encoding produces a different string than the one the runner is watching for, so the transformed output prints in plain text even though the original secret would have been masked.

Why is writing a secret to $GITHUB_ENV or $GITHUB_OUTPUT a separate risk?

Writing a value there exports it to later steps, but does not register it with the runner's masking list on its own. Only the exact string of a value GitHub already knows is a declared secret gets auto-masked. A freshly derived value — an OAuth token minted using a secret, a signed URL, a session cookie — is not on that list at all, so the first later step that echoes it prints it in plain text unless the workflow calls ::add-mask:: on it first.

Does this tool fetch anything from GitHub for me?

No. It makes no network calls at all — nothing here can, because nothing here talks to a server. Paste your workflow YAML and the scan runs entirely in this page.

Does a clean scan mean my secrets are safe?

No. This checks two specific, well-documented patterns within a single step's YAML text. It is not a security audit and makes no claim about your secrets' overall safety. A transform split across two steps, a custom action that re-exports a secret internally, or a transform utility this tool does not recognise are all outside what a regex/indentation scan can see.

More: why GitHub Actions secret masking fails on a transformed value, and this checker vs. reading the workflow by eye.