Open source · Application security

Writing a secret-detection rule for gitleaks: what a pattern has to prove

Most of the work in a secret-detection rule is not the regex. It is establishing what the token actually looks like, and then deciding what you refuse to flag.

In July 2026 I opened a pull request against gitleaks, the secret scanner, adding two rules for Supabase credentials. Writing it taught me more about detection engineering than any amount of reading about regular expressions would have, mostly because the regex turned out to be the smallest part of the job.

Status, stated plainly: PR #2226 is open and unmerged as of August 2026. Nothing below should be read as an accepted contribution. It is a description of how I approached the problem and what I would do differently, and it is written that way because a pending pull request is a pending pull request.

The gap

A secret scanner is a list of rules, and a rule is only as good as the evidence behind its pattern. The starting observation was simple enough to check: grep -i supabase config/gitleaks.toml returned nothing. Supabase's legacy anon and service_role keys are JSON Web Tokens, so the existing generic jwt rule already catches those. But the current-generation keys are not JWTs, and nothing in the default config matched them.

Two credentials matter here, for different reasons.

  1. The secret API key, prefixed sb_secret_. It is the replacement for the legacy service_role JWT and it bypasses Row Level Security, which means it is effectively full read and write access to a project's database, storage and auth.
  2. The personal access token, prefixed sbp_. It is used by the Management API and by the supabase CLI through SUPABASE_ACCESS_TOKEN. It is account-scoped rather than project-scoped, so one leak reaches every project the owner can reach.

Establishing the shape, instead of guessing it

Supabase documents the prefixes. It does not document the suffix. That is the normal situation, and it is where a rule either becomes trustworthy or becomes a source of noise for everyone who runs the scanner.

The temptation is to write something permissive — sb_secret_\w+ — and move on. That is a bad rule for a reason worth naming: an over-broad pattern does not fail loudly. It fails by producing findings that are mostly wrong, and a scanner whose findings are mostly wrong gets muted, at which point it stops catching the real ones too. The cost of a false positive is not one wasted minute. It is the credibility of every future alert.

So I sampled real occurrences instead. For sb_secret_, the prefix is followed by 22 base64url characters — 16 random bytes — plus an eight-character checksum segment, which is 31 characters in total counting the inner underscore. Supabase's own local-development output in a public discussion thread shows exactly that shape, and publicly visible sb_publishable_ keys have the identical 22 + _ + 8 structure. Because the underscore lives inside the character class, the separator is covered without needing a second group.

For sbp_, the prefix is followed by 40 lowercase hexadecimal characters. Public code turns up both sbp_<40 hex> and a versioned sbp_v0_<40 hex>, so the version segment has to be optional:

sbp_(?:v[0-9]_)?[a-f0-9]{40}

This is the part I would most want a junior engineer to take away. An existing detector for the same credential — TruffleHog's — uses \b(sbp_[a-z0-9]{40})\b. That cannot match the sbp_v0_ form at all, because the underscore falls outside its character class. Copying a pattern from another tool because it is already in production would have shipped a rule that silently misses real tokens. Prior art is a starting point for checking, not a substitute for it.

What the rule deliberately does not catch

Supabase also issues sb_publishable_ keys. They are designed to ship in client-side JavaScript. Flagging one would be a false positive by definition, in the same way that a Stripe pk_live_ publishable key is not a leak.

I did not simply omit it. I added it as an explicit false-positive test case, so the intent is pinned in the test suite rather than living in a reviewer's memory. Six months from now, when somebody notices that the scanner ignores a key that looks alarming and "fixes" it, the failing test is the thing that explains why it was left alone. A negative test is documentation that cannot go stale.

How the rule is validated

gitleaks has a useful convention: each rule ships with generated true positives and hand-written false positives, and a validation helper runs both at config-generation time. The true positives are produced by a helper that embeds a synthetic secret in every configuration and language shape the scanner expects to encounter — an environment file, a YAML value, a string literal in several languages — so the rule is tested against the wrapping, not just the token.

The false positives I wrote cover four failure modes:

  • a placeholder of the exactly correct length (a row of x characters), which is what a README or a template contains;
  • a token that is too short;
  • uppercase hex, which is not the format Supabase issues;
  • the publishable key described above.

The first of those is handled by an entropy threshold rather than by the regex. A rule that matches shape alone will always fire on documentation. Shape plus a minimum entropy is what separates "this looks like a token" from "this is random enough to be one".

One small detail with an outsized payoff: none of the sample values in the file are written as string literals. They are assembled at runtime from a prefix constant plus a generated suffix. Otherwise the rules file itself trips secret scanners, including this one, which is an embarrassing way to fail CI.

What I would tell someone opening their first upstream PR

Three things, none of which are about code.

Write the reasoning down in the description. A maintainer reviewing a regex has no way to tell a sampled pattern from a guessed one unless you say which it is and show the evidence. The description is where the review actually happens.

Say what you left out and why. An explicit non-goal removes the most likely review question before it is asked.

Do not describe an open pull request as a contribution to the project. It is a proposal. Maintainers of widely used security tools are conservative about default configs for good reasons, and being merged is not the same as being submitted. I would rather have this page be accurate in January than flattering in August.

If you are working on adjacent things, the notes on integrating third-party APIs cover the credential-handling side of the same problem, and the piece on reproducible build timestamps walks through a second open pull request in the same supply-chain tooling space.