Skip to content

Repository files navigation

Token Leak Scanner

A small Windows (WPF) tool that sweeps a codebase for specific strings that shouldn't be there — internal names, customer domains, sample keys, private markers — the kind of thing that quietly ends up in otherwise-generic code, comments, and test fixtures.

It finds and surfaces; it doesn't judge or fix. Every hit is a candidate for a human to review, shown with its file, its line, and whether it sits in source vs. test and code vs. comment. You decide what to do with it.

The list of things to look for lives in tokens.json and is entirely yours to define — plain strings, case-sensitive matches, or regex.

Token Leak Scanner scanning its own source for the word "leak"

Dogfooding: the scanner run against its own repository. Every hit is the literal string "leak" in its own code and docs — and nothing else, because its own private token list and excludes keep it out of the results.


What this does and doesn't do

Token Leak Scanner is an aid for a human review, not a guarantee. It searches for exactly the strings you define, in exactly the file types you configure — nothing more.

  • You define what it looks for. It finds the tokens listed in tokens.json; it has no built-in knowledge of what's sensitive. A string you didn't add is a string it won't find.
  • You define where it looks. It scans the extensions in includeExtensions and skips excludeDirectories and excludeFiles. Anything outside that scope is not examined.
  • Classification is a heuristic. Source/Test and Code/Comment tagging is a fast approximation, not a full parser, and can occasionally mislabel a hit.
  • Your token list is your responsibility to keep private. Sensitive tokens belong in tokens.private.json (git-ignored), not the committed tokens.json. The tool loads and saves to the private file when it exists, but it can't guarantee your repo actually ignores it — confirm *.private.json is in your .gitignore and not already tracked from an earlier commit.
  • A clean result is not proof of anything. "No findings" means "none of your tokens matched in your configured scope" — it does not mean the codebase is free of sensitive strings.

Treat every run as a prompt to review, and treat your tokens.json and scan scope as the thing that determines how much it can help. The responsibility for what ships stays with you.


Download

Grab the latest TokenLeakScanner.exe from the Releases page and run it — no install needed.

The build is self-contained (the .NET runtime and WPF are bundled into the exe), so it runs on a clean 64-bit Windows machine with nothing installed. Every release is provenance-attested and ships with a SHA-256 checksum, so you can verify the download came from this repository and wasn't tampered with:

gh attestation verify TokenLeakScanner.exe --repo sascha-codeforfun/TokenLeakScanner

On first launch Windows SmartScreen may warn about an unsigned app — choose More info → Run anyway. That's expected for an unsigned binary; the attestation above is the stronger, verifiable trust signal.

Requirements to run: 64-bit Windows. Nothing else — the runtime is bundled.


How to use

  1. Scan folder — pick the repo or folder to sweep.
  2. Tokens file — defaults to the tokens.json next to the exe; Browse to point at your own. (If a tokens.private.json sits beside it, that's loaded instead — see Private token lists.)
  3. Scan — results stream into the grid live as they're found. A scan runs in the background and can be stopped at any time with Cancel (findings collected so far are kept).
  4. Filter (free text) matches against the token, the file path, and the matched line; Show narrows by category — All, Source only, Test only, Comments only, or Code only.
  5. Double-click a finding to open that file at the exact line in Visual Studio (see Jumping to a finding).
  6. Export CSV saves whatever is currently visible, filters applied (see Exporting results).

The status bar shows how the current token list was loaded (including (private override) when the private file is active) and a live count: N shown / M total • K tokens loaded.


Editing the token list

Two ways to change what the scanner looks for, both of which write straight back to the currently active tokens file (so if a private override is loaded, edits go to the private file — never the committed one):

  • Add token row — type a value (optionally a description, and tick Case-sensitive or Regex), click Add & Save, and it's appended and saved immediately.
  • Edit tokens dialog — a full grid for bulk editing:
    • Add and delete rows (multi-select delete supported).
    • Edit each token's value, description, case-sensitivity, and regex flag inline.
    • A separate box for excluded-file glob patterns (one per line) that maps to excludeFiles.
    • Regex patterns are validated before saving — an invalid pattern is flagged and the dialog stays open so you can fix it, so one typo can't quietly break every scan.
    • Changes are made on a working copy: Cancel discards everything, and only Save commits.

Configuring what to look for (tokens.json)

The app reads and writes this file itself (camelCase keys, indented, null fields omitted), so anything you set via Add token or Edit tokens ends up here in the same shape:

{
  "tokens": [
    { "value": "AcmeCorp", "description": "why it shouldn't leak" },
    { "value": "TODO-INTERNAL", "caseSensitive": true },
    { "value": "sk-[A-Za-z0-9]{16,}", "isRegex": true }
  ],
  "includeExtensions": [".cs", ".xaml", ".razor", ".ts", ".js", ".json", ".xml", ".config", ".txt", ".md"],
  "excludeDirectories": ["bin", "obj", ".git", ".vs", "node_modules", "packages"],
  "excludeFiles": ["*-private.json", "*.secrets.*", "src/legacy/*.cs"],
  "testPathMarkers": ["test", "tests", "fixture", "fixtures", "spec", "specs"]
}
Field Purpose
tokens The strings to search for. Each has a value; caseSensitive (default false) and isRegex (default false) are optional per token, and description is a free-text note. When isRegex is true, value is a .NET regular expression.
includeExtensions Only files with these extensions are scanned. Default: .cs .xaml .razor .ts .js .json .xml .config .txt .md.
excludeDirectories Directory names skipped entirely (case-insensitive). Default: bin obj .git .vs node_modules packages.
excludeFiles Glob patterns for individual files to skip (case-insensitive, * and ? supported). Each pattern is tested against both the file name and the path relative to the scan root — so *-private.json, *.secrets.*, and src/legacy/*.cs all work. Also editable in the Edit tokens dialog. Default: none.
testPathMarkers Path/name fragments that mark a file as a test (see classification below). Default: test tests fixture fixtures spec specs.

Everything except tokens is optional and falls back to the defaults shown above if omitted.


Private token lists

Your token list itself can be sensitive — it may name the very internal things you're watching for. So the tool keeps a public list and a private one separate:

  • Commit the shared tokens.json.
  • Keep your own local list in tokens.private.json next to it.

If tokens.private.json exists, the app loads it instead of tokens.json automatically — on startup, and whenever you Browse or Reload to a tokens.json that has a private sibling. Browsing directly to a *.private.json file uses it as-is. The status bar shows (private override) while it's active, and every edit (Add & Save / Edit tokens) is written back to the private file, so your private tokens never touch the repo.

tokens.private.json (and any *.private.json) is listed in .gitignore, and the build copies it next to the exe when present. To point somewhere else entirely, just Browse to any .json file.


How findings are classified

Each hit is tagged two ways, combined into the Category shown in the grid as Source / Code, Source / Comment, Test / Code, or Test / Comment:

  • Source vs. Test — a file is treated as a test if any path segment matches a testPathMarkers entry (exactly, or as .marker / markers / .marker.), or the file name ends in test/tests or contains fixture/spec. Default markers: test, tests, fixture, fixtures, spec, specs.
  • Code vs. Comment — a per-line C-style comment detector marks which characters sit inside a comment (tracking multi-line /* ... */ blocks and ignoring // / /* that appear inside double-quoted strings). A hit is classed Comment when at least half of its characters fall in a comment region, otherwise Code.

That comment detector is a fast heuristic, not a full C# lexer: multi-line verbatim strings (@"...") and char literals can occasionally be misclassified. That's fine for a leak sweep, where the goal is to surface candidates for a human to review.


How scanning works

A few behaviours worth knowing:

  • Streaming and cancelable. Files are walked iteratively (skipping excludeDirectories and permission-denied folders without failing); each hit is reported the moment it's found, and Cancel stops the walk between files.
  • Unreadable files are skipped. Binary, locked, or otherwise unreadable files that happen to match an included extension are silently passed over rather than aborting the scan.
  • Matching. Literal tokens use an ordinal search (case-insensitive unless caseSensitive is set); regex tokens compile with culture-invariant options. All occurrences on a line are reported, each with its column.
  • A bad regex doesn't kill the run. An invalid regex token is skipped with a status warning and the rest of the scan continues (the Edit tokens dialog also validates regexes before saving, so this is a backstop).
  • Context is trimmed. The stored context line is trimmed and capped at 200 characters (with an ellipsis) — enough to identify the hit without dragging whole minified lines into the grid or CSV.
  • excludeFiles globs are whole-string anchored. A pattern must match the entire file name or the entire scan-root-relative path (with / separators), which is why *-private.json works but a bare private would not.

Exporting results

Export CSV writes whatever is currently visible in the grid — filters and category selection are respected, so you can export just the subset you care about. The file is UTF-8 with proper quoting (values containing commas, quotes, or newlines are escaped), so it opens cleanly in a spreadsheet. Columns:

Category, Token, Description, Line, Column, RelativePath, Context

Jumping to a finding in Visual Studio

Double-clicking a finding tries three things in order:

  1. A running Visual Studio instance whose open solution owns the file — it activates that instance, opens the file, and jumps to the exact line (via COM/DTE automation, with an OLE message filter so the call retries instead of failing when VS is busy).
  2. devenv /edit — if no suitable running instance is found, it asks Visual Studio to open the file (without the line jump).
  3. The OS default editor — as a last resort, it opens the file with its default handler.

The status bar reports which path was taken. This is Windows/Visual Studio specific; outside that setup you'll land on step 3.


Build from source

Requirements: the .NET 10 SDK on Windows (WPF only builds and runs on Windows), and — if you use the IDE — a Visual Studio version with .NET 10 support (Visual Studio 2022 17.14+ or Visual Studio 2026, which also handle the .slnx solution format).

cd TokenLeakScanner
dotnet run                 # build and launch
dotnet build -c Release    # release build

Or open TokenLeakScanner.slnx in Visual Studio and press F5.

To produce the self-contained single-file exe locally:

dotnet publish TokenLeakScanner.csproj -c Release -r win-x64 --self-contained true ^
  -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true ^
  -p:EnableCompressionInSingleFile=true

Releases are built automatically by the GitHub Actions workflow in .github/workflows/main.yml when a release is published.


Extending

  • More languages / comment styles — swap the logic in Services/CodeScanner.BuildCommentMask.
  • Whole-word matching — wrap literal tokens as regex with \b...\b, or set isRegex: true.
  • CI use — the scanning logic in CodeScanner has no WPF dependency, so it can be lifted into a console tool that fails a build when matches are found.

License

Released under the MIT License — see LICENSE.txt.

About

Finds internal strings leaked into code, comments, and test fixtures. Expandable JSON token list, private overrides, double-click into VS. Built from plain-English prompts to Claude.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages