Credential coverage
These are API keys, access tokens and private key material, found by the same pii detector as the personal data and annotated under credential/… rather than pii/…. They are on by default and cost a scan nothing extra: the same tables, the same prefilter, the same streaming horizon. A rule can select the whole namespace, since a.category == "credential" covers every row here, including ones added after the rule was written. The namespace exists for that reason.
19 recognizers · 19 entity types · 22 patterns · 1 checksum-validated.
What carries a row
Section titled “What carries a row”A literal prefix carries a row, in every case but one. AKIA, ghp_, sk-ant-, glpat- do not otherwise occur in prose, so precision comes from the prefix rather than from a shape, the scores are high, and an approximate length costs little. Every row is case-sensitive for the same reason. akia is not a key, and matching it would throw away most of what the prefix buys.
A prefix cannot tell a real key from a placeholder wearing its shape. ghp_ and thirty-six x’s is annotated like any other token. One row declares a checksum and refuses those outright; the rest are exempted in configuration, and Exempt placeholders from a credential rule is how.
Max span is the longest match a recognizer can produce, in bytes, and it is what a streamed response is held back by while this type is enabled (see the commit horizon); a † means a pattern has an open repeat charged at a cap rather than measured. Credentials are structurally long, and the widest here is 1082 bytes, though the horizon is the maximum over every enabled recognizer, so a row is only ever as expensive as the widest thing running beside it.
Entity types
Section titled “Entity types”| Entity | Recognizer | Patterns | Score | +context | Checksum | Max span | Context | Format |
|---|---|---|---|---|---|---|---|---|
ANTHROPIC_API_KEY |
AnthropicKeyRecognizer |
1 | 0.90 | 1.00 | 395 | anthropic, anthropic_api_key, claude, api key, x-api-key | betterleaks has converged on sk-ant-, a versioned kind, ninety-three characters and a trailing AA; Anthropic publishes no format. | |
AWS_ACCESS_KEY_ID |
AwsAccessKeyRecognizer |
1 | 0.90 | 1.00 | 68 | aws, aws_access_key_id, access key, secret access key, credentials, amazon | The published prefixes for access key ids (AKIA long-lived, ASIA temporary) with base32’s sixteen-character tail; principal ids (AIDA, AROA, AIPA) are excluded because they are not secret. | |
AWS_BEDROCK_API_KEY |
AwsBedrockKeyRecognizer |
2 | 0.90 | 1.00 | 1082 | bedrock, aws, aws_bearer_token_bedrock, api key, amazon, model | Bedrock’s bearer-token API keys, long-lived (ABSK) and short-lived; the short-lived pattern matches the published marker only, because the base64 that follows it has no documented length. | |
CEREBRAS_API_KEY |
CerebrasKeyRecognizer |
1 | 0.90 | 1.00 | 196 | cerebras, cerebras_api_key, api key, authorization, bearer | csk- and forty-eight characters; Cerebras publishes no format. | |
GITHUB_TOKEN |
GithubTokenRecognizer |
2 | 0.90 | 1.00 | 339 | github, gh, personal access token, pat, token, git | GitHub’s published format. Four-character prefix, thirty of entropy, six of base62 CRC32 checksum; the checksum is not declared because GitHub does not publish what it is computed over, and a wrong one would drop every real token. | |
GITLAB_TOKEN |
GitlabTokenRecognizer |
1 | 0.90 | 1.00 | 86 | gitlab, glpat, personal access token, ci_job_token, token | glpat- and twenty characters, the shape betterleaks carries. | |
GOOGLE_API_KEY |
GoogleApiKeyRecognizer |
2 | 0.90 | 1.00 | 191 | google, gcp, api key, googleapis, firebase, gemini, ai studio | The long-published AIza shape and AI Studio’s AQ.Ab8RN6 shape, filed as one entity because they are one vendor’s API key in two formats. | |
GROQ_API_KEY |
GroqKeyRecognizer |
1 | 0.90 | 1.00 | 212 | groq, groq_api_key, api key, authorization, bearer | gsk_ and fifty-two characters; Groq publishes no format, so the shape is upstream’s, with the case fold dropped from the prefix. | |
HUGGINGFACE_TOKEN |
HuggingFaceTokenRecognizer |
1 | 0.90 | 1.00 | 139 | huggingface, hugging face, hf_token, token, transformers | hf_ and thirty-four characters, the shape betterleaks carries; HuggingFace publishes no format. | |
NPM_TOKEN |
NpmTokenRecognizer |
1 | 0.90 | 1.00 | 148 | npm, npmrc, authtoken, registry, node | npm_ and thirty-six characters, the shape betterleaks carries. | |
OPENAI_API_KEY |
OpenAiKeyRecognizer |
1 | 0.90 | 1.00 | 654 | openai, openai_api_key, api key, authorization, bearer | Matched on the T3BlbkFJ infix, “OpenAI” in base64, which every key carries, so the row does not depend on a length OpenAI has changed twice. | |
OPENROUTER_API_KEY |
OpenRouterKeyRecognizer |
1 | 0.90 | 1.00 | 268 | openrouter, openrouter_api_key, api key, authorization, bearer | The versioned sk-or-v1- prefix and sixty-four hex characters; a future v2 prefix will miss rather than mismatch. | |
PERPLEXITY_API_KEY |
PerplexityKeyRecognizer |
1 | 0.90 | 1.00 | 197 | perplexity, pplx, perplexity_api_key, api key, authorization | pplx- and forty-eight characters; Perplexity publishes no format. | |
PISTRA_KEY |
PistraKeyRecognizer |
1 | 0.90 | 1.00 | yes | 203 | pistra, virtual key, api key, authorization, bearer | pistra’s own virtual key format. The prefix, a 43-character base62 body and six characters of CRC-32 over that body, checked here; it is the one row whose algorithm is ours, so the check is proven rather than inferred and a placeholder is refused. |
PRIVATE_KEY |
PrivateKeyBlockRecognizer |
1 | 0.95 | 1.00 | 160 | private key, pem, ssh, rsa, openssh, pgp, certificate | The opening armour line only. Matching to the closing line would make the streaming horizon the length of the key block, and the header already identifies the material. | |
SLACK_TOKEN |
SlackTokenRecognizer |
1 | 0.90 | 1.00 | 293 | slack, slack_token, bot token, webhook, xoxb | The xox prefix and its kind letter, then the dash-separated body; the shape betterleaks carries, loosened to one pattern across the kinds. | |
STRIPE_SECRET_KEY |
StripeKeyRecognizer |
1 | 0.90 | 1.00 | 404 | stripe, stripe_secret_key, secret key, restricted key, payment | Secret (sk_) and restricted (rk_) keys only; the publishable pk_ key is public by design and is deliberately not matched. | |
TOGETHER_API_KEY |
TogetherKeyRecognizer |
1 | 0.90 | 1.00 | 182 | together, togetherai, together_api_key, api key, authorization | The versioned tgp_v1_ prefix and forty-three characters; Together publishes no format. | |
XAI_API_KEY |
XaiKeyRecognizer |
1 | 0.90 | 1.00 | 484 | xai, grok, xai_api_key, api key, authorization | xai- and a body between seventy and a hundred and twenty characters, the range upstream converged on; xAI publishes no format. |
Scores
Section titled “Scores”Every row scores between 0.90 and 0.95 before anything corroborates it. It is high because a literal prefix is strong evidence, and uniform because the prefix is doing the work in every case rather than a shape that some rows have and others do not.
Every row also carries context words, and the enhancer’s increment of 0.35 takes all of them to the ceiling, so any of these near a match makes it 1.00. The practical consequence is that a score threshold is not a useful lever on credentials. There is no value that separates these rows from each other, and any value above 0.90 turns the whole namespace off. Select on a.category == "credential" and, where you need to be narrower, on a.entity_type or the matched text. See Stop a placeholder denying a request.
A score cannot tell you whether the key is real. A placeholder wearing a known prefix scores what a live key scores. The answer to that is an exemption you write and not a number you tune. The one exception is the row with a checksum, which refuses a wrong body outright. See the PII page’s score section for how scores work across both halves of this detector.
What is deliberately not here
Section titled “What is deliberately not here”Each of these is a decision rather than a gap, and each is argued in guardrails/pii/native/credentials.yaml:
- High-entropy strings. “A long random-looking base64 run” is not a pattern: no prefilter absorbs it, its streaming horizon is unbounded, and on the traffic an AI gateway carries it fires on hashes, ids, minified code and base64 images. This is the detector people learn to switch off, and one that is off is worse than one that was never claimed.
- Assignment shapes,
password = "…"andapi_key: …. The same failure at a lower score, and the thing being assigned is usually a placeholder. Their precision lives in scaffolding that exists in a configuration file and not in prose. - JWTs. Common, usually not secret, and in an AI gateway they arrive in the request’s own
authorizationheader on the way past. - AWS principal ids (
AIDA,AROA,AIPA) and secret access keys. The first are published in policy documents on purpose; the second are forty characters with no prefix, which is the high-entropy case wearing a name. - Providers with no prefix to carry them. DeepSeek, Mistral, Cohere and Voyage are all routed by this gateway and all absent, because every published pattern for them is the assignment shape above.
Scanning headers
Section titled “Scanning headers”Detectors read request bodies by default. apply_to: [header, query] is opt-in, and turning it on with these rows enabled means every request annotates the credential it just authenticated with. That is correct, and it is not what most people mean by “find credentials in my traffic”. Scope the rule rather than giving up the namespace: envelope annotations carry an addressable ref, so
annotations.exists(a, a.category == "credential" && a.ref != ".headers.authorization")keeps a credential in a query string, which every proxy on the path logs and which nothing else can see, while ignoring the one the request presented at the door.
Where the formats come from
Section titled “Where the formats come from”A vendor’s published format where there is one, and otherwise the shape betterleaks (MIT) has converged on across its corpus. The Format column says which for each row. Rows read from upstream record the rule they came from and quote the pattern it had at a pinned revision, so a vendor rotating a format or upstream narrowing a class after a false positive fails a test here rather than going unnoticed. make leak-rules reports what upstream has that this list does not.
Regenerate with make gen-docs; re-extract from upstream with make gen-recognizers.