Replaces secrets and credentials with stable placeholders inside Claude Code before the model or the session file ever sees them — prompts, tool results and…

Puts a placeholder where a secret used to be — before the model or the session file sees it — and puts the real value back only where the disk demands it.
Requires Claude Code ≥ 2.1.287. No network, no subprocess, no model call: a detector that transmits the value it is hiding would defeat itself, so a scan that needs egress is not a scan this mod runs.
claude plugin marketplace add ezoushen/claude-mods
claude plugin install prompt-redact@claude-mods
Or try it for one session without installing: claude --plugin-dir ./prompt-redact.
[REDACTED:LABEL#hash] before the prompt is queued. The transcript and prompt history show the placeholder.cat of a file holding a key hands the model a placeholder.session.append rewrites a stored row before both the model and the transcript see it, which closes the echo-back leak where the model restates a secret. Caveat: an organization account seats the builtin cc-plugin-sec-default outermost. That guard forwards past the user tier (next.to(e, "append")) for the rows the engine injects itself — the attachment, hook-context, compaction and skill tool-message doors, plus an organization plugin's own notes — so those four are not reached at user tier. Ordinary prompt, reply and tool-result rows are reached: the guard does not skip those doors. Two carriers have no hook at all, at any tier: prompt.context (where managed CLAUDE.md most likely travels) and skill.prompt. An @-mentioned file is not a stored row; it travels on the next carrier.prompt.attachment scrubs text another settings hook injects into the request (SessionStart/UserPromptSubmit additional context), which session.append never sees because it is not a stored row. A file referenced with @ also travels through this carrier. On an organization account this hook is skipped, and the file's content reaches the model unredacted — a measured leak, quantified under Known limitation.Edit, Write and NotebookEdit arguments, so an edit to a line holding a real key still matches the file on disk. Bash never receives a real value.A one-line band above the prompt shows what it has hidden, by label and count — never a value.
Five layers, auto-selected per machine and per row:
alg), IBAN mod-97, Luhn card numbers, SSA numbers under the issuance rules. These carry their own proof, so they need no entropy gate or context.tools/check-rules.mjs.keyword = value shape, with a tolerant prefix, a quote tolerated before the separator, and a low length floor..claude/prompt-redact.rules (one LABEL <javascript-regex> per line) is probed at session start and merged. A session that only has the built-in corpus starts silently; the readiness line appears when local rules or the optional classifier are loaded, and /redact answers on demand.Allowlisted (left untouched, because redacting them breaks the task): git and content hashes, UUID/ULID/ObjectId, lockfile integrity, bcrypt, SSH public keys, data: URIs, SRI, ARNs, account ids, IPs, emails, timestamps, versions, and the placeholder lexicon (changeme, ${ENV}, <your-key>, …).
Fail closed: a row whose scan throws or overruns its budget is withheld ([redact withheld this content: the redactor failed while scanning it]); a prompt that cannot be scanned is dropped rather than sent.
/redact counts by label, restore count, vault size
/redact on|off enable or disable scanning for this session
/redact tier strict|balanced|precision
/redact allow <re> never redact values matching <re>
/redact deny <re> always redact values matching <re>
/redact why how detection works, and this session's posture
strict redacts anything suspicious, balanced is the default, precision only clear-cut secrets. Rules can be added (.claude/prompt-redact.rules, one LABEL <regex> per line) and removed: userConfig.disabledRules or /redact rule off <id> switches a shipped rule off for the session, /redact rule on <id> puts it back, and /redact rule list shows what is off. /redact is also where on|off, tier, allow, deny and why live.
Configuration also lives in the plugin's userConfig (enabled, tier, minEntropy, scanBudgetMs), so /config can set it.
Everything above keeps the model from holding a real value — which also means the model cannot hand one to a command. A credential reaches the shell on one of three paths, each measured on a live Haiku session with the plugin loaded from source and a synthetic 35-character canary (never a real key). The placeholder that would stand in for it, [REDACTED:…#…], is 43 characters, so a command printing 35 proves the shell held the real value — the placeholder is not 35 characters long.
1. Export before launch. Bash inherits the launching shell's environment. With E2E_ENV_CANARY exported before Claude Code starts, the model was asked to run printf %s "$E2E_ENV_CANARY" | wc -c; it printed 35, the real value's length. The model sees the variable's name, runs the command, and reports a number: the value never enters its context, so there is nothing to redact in this path. grep -c on the session file returned 0, and tools/verify-transcript.mjs reported 0 model-facing, 0 tool-result, 0 residual occurrences.
2. The ! prefix. A command typed with ! runs in the same shell. !printf %s "$E2E_ENV_CANARY" | wc -c printed 35 in the same measured session, and the transcript check held: 0 model-facing, 0 tool-result, 0 residual. The stored row shows only the command line and its status line.
3. Through a file an edit tool writes. Type or paste the secret once — prompt redaction turns it into [REDACTED:LABEL#hash] across the prompt and the transcript. Then have Claude write the file you actually need:
> create env.sh with the line STRIPE_KEY=<the redacted placeholder>
The Write call carries the placeholder — the measured session's tool argument read STRIPE_KEY=[REDACTED:GENERIC_API_KEY#6997db91011b1685] — and rehydration swaps it for the real value in the file on disk, the one channel Edit, Write and NotebookEdit get (see the Edits bullet above). The shell then picks it up:
set -a; . ./env.sh; set +a # export all; source; stop exporting
printf %s "$STRIPE_KEY" | wc -c # → 35
The transcript check passed with 0 model-facing, 0 tool-result, 0 residual occurrences, and the transcript carried the two placeholders for the redacted values rather than either raw value.
Three things these paths do not permit:
Bash never receives a real value. Rehydration is scoped to Edit, Write and NotebookEdit (DECISIONS.md §8, which is what keeps a restored placeholder from becoming an exfiltration primitive). A literal secret typed into a Bash command becomes a placeholder before the shell sees it, so an inline curl -H "Authorization: Bearer …" with the secret cannot work — the command would carry the placeholder. That call is now refused outright rather than run as a silent no-op (see Teaching the model what a placeholder means).Bash at all. It must live in the environment before launch (path 1) or be materialised on disk (path 3). Handing the value to Claude in prose buys you neither.Write is the channel that materialises it, so treat the result as a secret file: chmod 600 env.sh, list it in the repository's .gitignore before the write, and delete it when the task is done.Captures: paths 1 and 2 in reports/evidence/06-operator-capture.txt and session 435fbaf4-ee44-45a4-b723-9556a00f5077.jsonl; path 3 in session abb84738-c36b-4c70-bcc3-86bf25c64e7f.jsonl.
A placeholder is opaque, and the model has to know both what it is and what it cannot do with it. Three layers, weakest to strongest:
skills/redacted-values/SKILL.md, also runnable as /prompt-redact:redacted-values. It sorts the three origins (the user's own message, tool output, the model's own reply) and states the correct move for each. Advisory by nature: Claude invokes it when the description matches, and can miss the turn it matters.prompt.compose / prompt.section append the opaque description to the system prompt, so the model is told on every turn what the placeholders are, whichever of the two hooks the engine reaches.set -a; . ./.env; set +a, or reference by variable name).tool.call refuses a Bash, WebFetch or WebSearch call whose arguments carry a placeholder. Those tools are deliberately never rehydrated, so without this a command would run with [REDACTED:…#…] as literal text and report success. The refusal names the file-loading route.The honest split: "stop and tell the user" cannot be forced as prose — a hook cannot dictate the model's words. What can be forced is the refusal, and a refusal with the instruction attached leaves the model no way to continue without surfacing it to the user.
On a machine with managed settings, cc-plugin-sec-default forwards some events past the user tier and Claude Code prints one line per event and kind. That line names the event, not the door, so it reads as if the whole hook were dead.
Cause: Claude Code's own injected rows, not another plugin. Measured with claude-mem disabled entirely: the line still appeared, with 2 bypassed session.append runs. The engine emits environment, model, skill_listing, date, instructions and session_context attachments at startup, and every one takes the attachment door the guard forwards. claude-mem's SessionStart context adds more of the same (upstream issue #1269 calls them saved_hook_context).
Since 0.3.2 this plugin no longer registers those doors. session.append is matched only for the doors a user-tier mod can actually read — prompt, command, response, tool-result, delivery, notice, queue — so the engine has no skipped hook of this plugin's to report. The injected doors were never readable at this tier, so no carrier is lost. The exclusion is unconditional — no registration names those doors, so a deployment seated above the guard would not receive them either.
Since 0.3.3, guardedDeployment removes the remaining three. At this tier the guard also forwards prompt.attachment, prompt.compose and prompt.section, so registering them produced one line each at startup for hooks that could never run. Set guardedDeployment: true in the plugin's config — pluginConfigs in user settings or, org-wide, in managed settings:
{ "pluginConfigs": { "prompt-redact@claude-mods": { "options": { "guardedDeployment": true } } } }
With that in place those three hooks are not registered at all: no skipped-hook line from this plugin, on any event. Measured end to end: 0 skip lines, prompt.submit x1, session.append x8, canary absent from the transcript.
The option stays off by default. On a machine with no policy mod it must stay off: there those three hooks are what deliver the opaque notice and scan engine attachments (origin.kind === "hook" context, which session.append never sees).
/redact why shows what did arrive. Measured on a quiet session before 0.3.2: 21 session.append runs reached the mod while 2–9 injected runs were bypassed; prompt.submit fired normally.
prompt.context (managed CLAUDE.md) and skill.prompt have no hook at all, at any tier, so those carriers remain uncovered regardless of seating.
Measured on a live Haiku session with tools/verify-transcript.mjs, plugin loaded from source, memory plugin excluded so no third party pre-injected text. The session exercised the typed-prompt and tool-result carriers, not an @-mentioned file (see item 1 below):
model-facing canary occurrences: 0
toolUseResult canary occurrences: 0
RESIDUAL (not hook-reachable): 0
rehydration: saved3.txt holds the real value
grep -c '<canary>' <session>.jsonl returns 0 for both canaries. The model never saw a real value, no tool sidecar kept one, and the file on disk still got the real key because rehydration fired.
Two things that number does not claim:
cc-plugin-sec-default seats outermost and forwards past the user tier on this mod's prompt.section, prompt.compose, prompt.attachment hooks and on the engine-injected session.append doors ("runs first and sent the event on past it"). Prompt, reply and tool-result rows still reach session.append; typed prompts and tool results stay covered by prompt.submit and tool.call.Registering this plugin org-wide from a GitHub marketplace (for example extraKnownMarketplaces + enabledPlugins in managed settings) makes it available to every member, but it does not change the seating: a plugin Claude Code copies into its cache counts as the person's, even when managed settings enable it. To seat it ahead of the guard, an administrator needs all of: a marketplace declared with a directory source on each machine, managed enabledPlugins, and prependPlugins listing this plugin before sec-default@builtin (the list replaces the default seating, so the guard must be named). The remote payload also needs managedSourcesBehavior: "merge", or a local managed-settings.json is ignored under first-wins.
For the attachment carrier this is not a theoretical caveat. Measured on this account (organization OAuth, live Haiku session, policy plugin outermost): a file holding two synthetic canaries was referenced from the prompt as an @-mention, and both canaries reached model-facing content unredacted — 2 raw occurrences of each canary in the session file, api-request-blob(role=system) at line 21. tools/verify-transcript.mjs exited non-zero:
FAIL GUARANTEE VIOLATED: canary appears in model-facing content x1 — line 21 (api-request-blob(role=system))
FAIL GUARANTEE VIOLATED: canary appears in model-facing content x1 — line 21 (api-request-blob(role=system))
2 verification failure(s)
prompt.attachment never fired for the event, and the platform reported at session start that its prompt.attachment hook was skipped because cc-plugin-sec-default runs first. So attachments are not safe to use with this mod on this deployment: @-mentioning a file hands its contents to the model unscrubbed. This cannot be fixed from inside the mod — a skipped hook cannot be made to fire from here. On an account where the hook does fire the same content would be scanned by the same detectors, but that path is not what this account measured. Captures: reports/evidence/05/attachment-probe.txt and session 219934f7-2129-4185-a6f5-973a584cfc47.jsonl.
tools/measure-recall.mjs). A bare 8-character alphabetic password with no keyword anchor has nothing for a rule to grip. The local classifier closes the shapeless-token tier (0% → 100%) but not this one, and it ships without weights: training needs data that leaves the machine, which this mod forbids.node tools/check-rules.mjs # corpus integrity: provenance, compile, bounded
claude plugin validate ./prompt-redact
claude plugin test ./prompt-redact # 80 tests
python3 tools/gen-rules.py # regenerate rules + RULES.md
Research record: ../research/prompt-redaction.md.
hooks/register.ts 1105 lines1// prompt-redact: put a placeholder where a secret used to be, before the model
2// and the session file ever see it, and put the real value back only where the
3// disk demands it.
4//
5// Design constraints that shaped every line below:
6//
7// * Reach L0. No network call, no subprocess, no model call -- the static
8// posture check greps for exactly those. A detector that transmits the value
9// it is trying to hide would defeat itself. The one capability probed is
10// filesystem access, for an optional local rules file.
11// * Fail closed. A row that throws or overruns its budget is withheld, never
12// stored raw.
13// * Rehydrate narrowly. Only `Edit`/`Write`/`NotebookEdit` get real values
14// back. `Bash` never does: otherwise any command the model writes becomes an
15// exfiltration primitive and the placeholder resolves on the way out.
16// * Deterministic placeholders, so an edit to a line holding a real key still
17// matches the file on disk.
18// * The vault lives in `$.state` only: session-scoped, survives a hot reload,
19// gone at exit, never written to disk or `$.store`.
20//
21// The state accessors below are written out one per key on purpose. The engine
22// requires every `$.state.get`/`$.state.set` reference to name its plugin and key
23// as string literals written at the call site, so the full state surface can be
24// listed from the compiled module. A generic `read($, key)` helper would hide it.
25
26import {
27 asRehydratable,
28 detectorsFor,
29 placeholderCount,
30 placeholdersIn,
31 rehydrate,
32 scrub,
33 scrubMessage,
34 type Rehydratable,
35 type ScrubHit,
36 type ScrubOptions,
37 type Tier,
38} from "./scrub.ts";
39import { parseClassifier, type ClassifierModel } from "./classifier.ts";
40import { RULES } from "./rules.ts";
41
42const WITHHELD =
43 "[redact withheld this content: the redactor failed while scanning it]";
44
45/** Fields a rewrite must not touch; the engine refuses them anyway. */
46const RESERVED = new Set(["tool", "tool_use_id", "agentId"]);
47
48/** Tools whose arguments may receive a real value back. */
49const REHYDRATE_TOOLS = ["Edit", "Write", "NotebookEdit"] as const;
50
51/**
52 * Tools that must never carry a placeholder onward. Rehydration is deliberately
53 * absent here, so a placeholder that reached one of these would run or travel as
54 * literal text: the command "succeeds" while doing nothing, which is worse than
55 * refusing. A refusal can carry the route that works; a silent no-op cannot.
56 */
57const GUARDED_TOOLS = ["Bash", "WebFetch", "WebSearch"] as const;
58
59const OPAQUE_NOTICE =
60 "\n\nSome values in this conversation appear as `[REDACTED:<LABEL>#<hash>]`. " +
61 "They are opaque placeholders the session minted for credentials, API keys and " +
62 "private keys that never left this machine. Treat them as opaque identifiers: " +
63 "you may reference them, and copy them into `Edit`/`Write`/`NotebookEdit` " +
64 "arguments (the real value is restored there, so an edit to a line holding a key " +
65 "still matches the file), but you cannot read or reveal their contents, and " +
66 "asking the person to paste the real value would defeat the point. Do not guess " +
67 "or reconstruct them. They are stable for the whole session.";
68
69const LOCAL_RULES_PATHS = [".claude/prompt-redact.rules", ".prompt-redact.rules"];
70
71/**
72 * Where an L2 weights file may sit. Both are project-local: the model is read
73 * with `$.fs` from the session's own directory, so nothing about the value being
74 * judged ever leaves the machine.
75 */
76const MODEL_PATHS = [".claude/prompt-redact.model.json", ".prompt-redact.model.json"];
77
78/** Re-probe interval for the optional model: a trainer can edit it mid-session. */
79const MODEL_CACHE_MS = 30_000;
80
81/** Section id the notice is appended under; a plugin's own ids are namespaced. */
82const NOTICE_SECTION_ID = "prompt-redact:notice";
83
84const TIERS: readonly Tier[] = ["strict", "balanced", "precision"];
85
86/**
87 * The interception surface, in the order `/redact why` lists it.
88 *
89 * `prompt.submit` and `tool.call` carry the typed prompt and the tool results on
90 * every deployment. The other four are the prompt-content hooks a managed
91 * account's builtin policy plugin can keep from ever reaching this mod: the
92 * platform seats such a plugin outermost, and a run it forwards past the user
93 * tier (`next.to(e, "append")`) means this mod never sees that run. Whether that happened is written only by the
94 * platform, outside this process (its skip notice reaches the transcript or the
95 * debug log and no hook), so the report below can state exactly one thing:
96 * how many runs of each hook arrived. It can never state that a carrier was
97 * covered, because a skipped run is indistinguishable from no run.
98 */
99const INTERCEPTION_HOOKS = [
100 "prompt.submit",
101 "tool.call",
102 "session.append",
103 "prompt.attachment",
104 "prompt.compose",
105 "prompt.section",
106] as const;
107type InterceptionHook = (typeof INTERCEPTION_HOOKS)[number];
108
109/**
110 * Rule ids the engine honours that are not rows in the generated corpus:
111 * `local-classifier` switches the optional L2 stage, the other three switch
112 * detector families delivered alongside it. They are validated by the command
113 * exactly like a corpus id, so a real id never reports "no such rule".
114 */
115const ENGINE_RULE_IDS = ["local-classifier", "url-userinfo", "basic-auth", "password-value"] as const;
116
117// --- state surface, one literal reference per key ---------------------------
118
119async function getSalt($: any): Promise<string | undefined> {
120 try {
121 const got = await $.state.get({ plugin: "prompt-redact", key: "salt" });
122 return typeof got?.value === "string" ? got.value : undefined;
123 } catch {
124 return undefined;
125 }
126}
127async function setSalt($: any, value: string): Promise<void> {
128 try {
129 await $.state.set({ plugin: "prompt-redact", key: "salt" }, value);
130 } catch {
131 /* a failed state write must never block the redaction itself */
132 }
133}
134
135async function getVault($: any): Promise<Record<string, string>> {
136 try {
137 const got = await $.state.get({ plugin: "prompt-redact", key: "vault" });
138 return got?.value && typeof got.value === "object" ? (got.value as Record<string, string>) : {};
139 } catch {
140 return {};
141 }
142}
143async function setVault($: any, value: Record<string, string>): Promise<void> {
144 try {
145 await $.state.set({ plugin: "prompt-redact", key: "vault" }, value);
146 } catch {
147 /* best-effort */
148 }
149}
150
151async function getCounts($: any): Promise<Record<string, number>> {
152 try {
153 const got = await $.state.get({ plugin: "prompt-redact", key: "counts" });
154 return got?.value && typeof got.value === "object" ? (got.value as Record<string, number>) : {};
155 } catch {
156 return {};
157 }
158}
159async function setCounts($: any, value: Record<string, number>): Promise<void> {
160 try {
161 await $.state.set({ plugin: "prompt-redact", key: "counts" }, value);
162 } catch {
163 /* best-effort */
164 }
165}
166
167async function getRestored($: any): Promise<number> {
168 try {
169 const got = await $.state.get({ plugin: "prompt-redact", key: "restored" });
170 return typeof got?.value === "number" ? got.value : 0;
171 } catch {
172 return 0;
173 }
174}
175async function setRestored($: any, value: number): Promise<void> {
176 try {
177 await $.state.set({ plugin: "prompt-redact", key: "restored" }, value);
178 } catch {
179 /* best-effort */
180 }
181}
182
183async function getEnabled($: any): Promise<boolean | undefined> {
184 try {
185 const got = await $.state.get({ plugin: "prompt-redact", key: "enabled" });
186 return typeof got?.value === "boolean" ? got.value : undefined;
187 } catch {
188 return undefined;
189 }
190}
191async function setEnabled($: any, value: boolean): Promise<void> {
192 try {
193 await $.state.set({ plugin: "prompt-redact", key: "enabled" }, value);
194 } catch {
195 /* best-effort */
196 }
197}
198
199async function getTier($: any): Promise<Tier | undefined> {
200 try {
201 const got = await $.state.get({ plugin: "prompt-redact", key: "tier" });
202 const value = got?.value;
203 return TIERS.includes(value as Tier) ? (value as Tier) : undefined;
204 } catch {
205 return undefined;
206 }
207}
208async function setTier($: any, value: Tier): Promise<void> {
209 try {
210 await $.state.set({ plugin: "prompt-redact", key: "tier" }, value);
211 } catch {
212 /* best-effort */
213 }
214}
215
216async function getAllow($: any): Promise<string[]> {
217 try {
218 const got = await $.state.get({ plugin: "prompt-redact", key: "allow" });
219 return Array.isArray(got?.value) ? (got.value as string[]) : [];
220 } catch {
221 return [];
222 }
223}
224async function setAllow($: any, value: string[]): Promise<void> {
225 try {
226 await $.state.set({ plugin: "prompt-redact", key: "allow" }, value);
227 } catch {
228 /* best-effort */
229 }
230}
231
232async function getDeny($: any): Promise<string[]> {
233 try {
234 const got = await $.state.get({ plugin: "prompt-redact", key: "deny" });
235 return Array.isArray(got?.value) ? (got.value as string[]) : [];
236 } catch {
237 return [];
238 }
239}
240async function setDeny($: any, value: string[]): Promise<void> {
241 try {
242 await $.state.set({ plugin: "prompt-redact", key: "deny" }, value);
243 } catch {
244 /* best-effort */
245 }
246}
247
248/** Cached probe result; `null` model means L1 only. */
249let modelCache: { at: number; model: ClassifierModel | null } | null = null;
250
251async function getDisabled($: any): Promise<string[] | undefined> {
252 try {
253 const got = await $.state.get({ plugin: "prompt-redact", key: "disabled" });
254 return Array.isArray(got?.value) ? (got.value as string[]) : undefined;
255 } catch {
256 return undefined;
257 }
258}
259async function setDisabled($: any, value: string[]): Promise<void> {
260 try {
261 await $.state.set({ plugin: "prompt-redact", key: "disabled" }, value);
262 } catch {
263 /* best-effort */
264 }
265}
266
267async function getFires($: any): Promise<Record<string, number>> {
268 try {
269 const got = await $.state.get({ plugin: "prompt-redact", key: "hookFires" });
270 return got?.value && typeof got.value === "object" ? (got.value as Record<string, number>) : {};
271 } catch {
272 return {};
273 }
274}
275async function setFires($: any, value: Record<string, number>): Promise<void> {
276 try {
277 await $.state.set({ plugin: "prompt-redact", key: "hookFires" }, value);
278 } catch {
279 /* best-effort */
280 }
281}
282
283/**
284 * Whether this prompt already carries the opaque-placeholder notice. Two hooks
285 * deliver it (`prompt.section` and `prompt.compose`), and both fire on a surface
286 * where neither is pre-empted, so the first one to land sets this and the second
287 * stands down.
288 */
289async function getNoticeShown($: any): Promise<boolean> {
290 try {
291 const got = await $.state.get({ plugin: "prompt-redact", key: "noticeShown" });
292 return got?.value === true;
293 } catch {
294 return false;
295 }
296}
297async function setNoticeShown($: any, value: boolean): Promise<void> {
298 try {
299 await $.state.set({ plugin: "prompt-redact", key: "noticeShown" }, value);
300 } catch {
301 /* best-effort */
302 }
303}
304
305/**
306 * How many runs of each interception hook reached this code. This is the only
307 * in-process evidence there is: an engine-skipped run never enters a hook, and
308 * the platform's skip notice is written outside this process, where no hook
309 * can read it.
310 *
311 * One read plus one write per delivered run. `session.append` runs on every
312 * stored row but already reads several state keys per row, so this stays in the
313 * cost class the hot path already pays.
314 */
315async function markObserved($: any, hook: InterceptionHook): Promise<void> {
316 try {
317 const fires = await getFires($);
318 await setFires($, { ...fires, [hook]: (fires[hook] ?? 0) + 1 });
319 } catch {
320 /* a failed state write must never block the hook protecting the row */
321 }
322}
323
324// --- helpers ----------------------------------------------------------------
325
326/**
327 * A per-session salt. A placeholder minted under one salt is meaningless under
328 * another, so a placeholder copied in from a different session restores nothing.
329 */
330function newSalt(): string {
331 let out = "";
332 for (let i = 0; i < 4; i++) out += Math.random().toString(36).slice(2, 10);
333 return `${out}${Date.now().toString(36)}`;
334}
335
336function compile(source: string): RegExp | null {
337 try {
338 return new RegExp(source, "i");
339 } catch {
340 return null;
341 }
342}
343
344/**
345 * Which sources may steer detection on this run.
346 *
347 * `repo` covers the project-local rules file and the optional classifier: both
348 * are read from the session's own directory, so a cloned repository controls
349 * them. That is the point where this mod sits in the person's own tier. It is
350 * not acceptable where the mod is the organization's, because a repository would
351 * then gain a say over what the organization put in front of the model.
352 */
353type Sourcing = {
354 readonly repo: boolean;
355 /** Session allow/deny lists: the person's steering, applied to user content only. */
356 readonly sessionLists: boolean;
357};
358
359/** Tiers where this mod counts as the organization's, not the person's. */
360function isOrgSeated(next: unknown): boolean {
361 const tier = (next as { origin?: { tier?: unknown } } | undefined)?.origin?.tier;
362 return tier === "prepend" || tier === "append";
363}
364
365function sourcingFor(next: unknown, sessionLists: boolean): Sourcing {
366 return { repo: !isOrgSeated(next), sessionLists };
367}
368
369async function optionsFor(
370 $: any,
371 salt: string,
372 tier: Tier,
373 minEntropy: number,
374 budgetMs: number,
375 disabledRules: readonly string[],
376 sourcing: Sourcing,
377): Promise<ScrubOptions> {
378 return {
379 salt,
380 tier,
381 minEntropy,
382 // Repository-supplied detection only where this mod sits in the person's tier.
383 extraRules: sourcing.repo ? await localRules($) : [],
384 allowExtra: sourcing.sessionLists ? (await getAllow($)).map(compile).filter((r): r is RegExp => r !== null) : [],
385 deny: sourcing.sessionLists ? (await getDeny($)).map(compile).filter((r): r is RegExp => r !== null) : [],
386 budgetMs,
387 classifier: sourcing.repo ? await localClassifier($) : null,
388 disabledRules: (await getDisabled($)) ?? disabledRules,
389 };
390}
391
392/**
393 * Merge a local rules file when one exists. This is the capability probe: the
394 * redactor adapts to what the machine already has, and degrades silently to the
395 * built-in corpus when it has nothing. Format, one rule per line:
396 *
397 * LABEL_NAME <javascript-regex>
398 */
399async function localRules($: any): Promise<ScrubOptions["extraRules"]> {
400 for (const rel of LOCAL_RULES_PATHS) {
401 try {
402 if (!(await $.fs.exists(rel))) continue;
403 const raw = await $.fs.read(rel);
404 const text = typeof raw === "string" ? raw : String(raw ?? "");
405 const rules = text
406 .split("\n")
407 .map((line) => line.trim())
408 .filter((line) => line.length > 0 && !line.startsWith("#"))
409 .flatMap((line) => {
410 const separator = line.indexOf(" ");
411 if (separator < 1) return [];
412 const label = line.slice(0, separator);
413 const pattern = line.slice(separator + 1).trim();
414 if (!pattern) return [];
415 if (!compile(pattern)) return [];
416 return [
417 {
418 id: `local:${label}`,
419 label: label.toUpperCase().replace(/[^A-Z0-9]+/g, "_"),
420 regex: pattern,
421 flags: "i",
422 groups: 0,
423 secretGroup: null,
424 entropy: null,
425 keywords: [] as string[],
426 allow: [] as string[],
427 kind: "generic" as const,
428 source: `local file ${rel}`,
429 },
430 ];
431 });
432 if (rules.length > 0) return rules;
433 } catch {
434 // Unreadable or unparseable rules file: fall back to the built-in corpus.
435 }
436 }
437 return [];
438}
439
440/**
441 * Probe for an optional L2 model and return it, or null.
442 *
443 * This is the ladder's capability probe for the prose/multilingual tier: the
444 * model is a project-local weights file, evaluated in-process by classifier.ts.
445 * Absent, unreadable or malformed, this returns null and the pipeline stays at
446 * L1 -- silently, because a missing model is the normal case, not an error.
447 */
448async function localClassifier($: any): Promise<ClassifierModel | null> {
449 const now = Date.now();
450 if (modelCache && now - modelCache.at < MODEL_CACHE_MS) return modelCache.model;
451 let model: ClassifierModel | null = null;
452 for (const rel of MODEL_PATHS) {
453 try {
454 if (!(await $.fs.exists(rel))) continue;
455 const raw = await $.fs.read(rel);
456 const text = typeof raw === "string" ? raw : String(raw ?? "");
457 const parsed = parseClassifier(text);
458 if (parsed) {
459 model = parsed;
460 break;
461 }
462 } catch {
463 // Unreadable weights are not a reason to stop redacting: stay at L1.
464 }
465 }
466 modelCache = { at: now, model };
467 return model;
468}
469
470function totals(hits: readonly ScrubHit[], counts: Record<string, number>): Record<string, number> {
471 const next = { ...counts };
472 for (const hit of hits) next[hit.label] = (next[hit.label] ?? 0) + 1;
473 return next;
474}
475
476async function recordHits($: any, hits: readonly ScrubHit[]): Promise<void> {
477 if (hits.length === 0) return;
478 const vault = await getVault($);
479 for (const hit of hits) vault[hit.placeholder] = hit.value;
480 await setVault($, vault);
481 await setCounts($, totals(hits, await getCounts($)));
482}
483
484function report(
485 counts: Record<string, number>,
486 restored: number,
487 enabled: boolean,
488 tier: Tier,
489 vaultSize: number,
490): string {
491 const lines = [`prompt-redact — ${enabled ? "on" : "OFF"} (tier: ${tier})`];
492 const labels = Object.entries(counts).sort((a, b) => b[1] - a[1]);
493 if (labels.length === 0) {
494 lines.push("hid nothing yet this session.");
495 } else {
496 const total = labels.reduce((n, [, count]) => n + count, 0);
497 lines.push(`hid ${total} value${total === 1 ? "" : "s"} this session:`);
498 for (const [label, count] of labels) lines.push(` ${label} x${count}`);
499 }
500 lines.push(`restored ${restored} placeholder${restored === 1 ? "" : "s"} into edit arguments.`);
501 lines.push(`vault: ${vaultSize} value${vaultSize === 1 ? "" : "s"} in session state (never on disk).`);
502 lines.push("Values are never shown here — only labels and counts.");
503 return lines.join("\n");
504}
505
506/**
507 * The interception posture: how many runs each hook delivered this session, and
508 * which have never reached us. Names, counts and a fixed reason only — never a
509 * value, a placeholder or a hash.
510 *
511 * The word "armed" is never printed. It is the word a live session contradicted:
512 * counts say what ran here; they say nothing about runs the platform sent past
513 * the mod, so presenting them as coverage would repeat exactly that lie.
514 */
515function interceptionPosture(fires: Readonly<Record<string, number>>): string[] {
516 const known = INTERCEPTION_HOOKS.filter((hook) => typeof fires[hook] === "number" && fires[hook] > 0);
517 const unseen = INTERCEPTION_HOOKS.filter((hook) => !(typeof fires[hook] === "number" && fires[hook] > 0));
518 const lines = [`interception hooks: ${known.length} fired this session, ${unseen.length} not observed`];
519 lines.push(
520 ` fired this session: ${
521 known.length === 0 ? "none" : known.map((hook) => `${hook} x${fires[hook]}`).join(", ")
522 }`,
523 );
524 if (unseen.length > 0) {
525 lines.push(
526 ` not observed: ${unseen.join(", ")} (an outer policy plugin can keep an event from reaching this mod, or no such event has occurred yet — this mod cannot see which)`,
527 );
528 }
529 lines.push(
530 " observed firing is not coverage: an outer policy plugin can send the event past any of these hooks without this mod seeing it, so the counts name only the runs that arrived",
531 );
532 return lines;
533}
534
535type Cfg = {
536 readonly enabled: boolean;
537 readonly tier: Tier;
538 readonly minEntropy: number;
539 /** Wall-clock ceiling for scanning one row; exceeding it fails closed. */
540 readonly budgetMs: number;
541 /** Rule ids off at session start, from the manifest's `disabledRules`. */
542 readonly disabledRules: readonly string[];
543};
544
545async function posture($: any, cfg: Cfg): Promise<{ enabled: boolean; tier: Tier; salt: string }> {
546 const salt = (await getSalt($)) ?? "";
547 const enabled = (await getEnabled($)) ?? cfg.enabled;
548 const tier = (await getTier($)) ?? cfg.tier;
549 return { enabled, tier, salt };
550}
551
552async function initialized($: any, cfg: Cfg): Promise<void> {
553 if (!(await getSalt($))) await setSalt($, newSalt());
554 if ((await getEnabled($)) === undefined) await setEnabled($, cfg.enabled);
555 if (!(await getTier($))) await setTier($, cfg.tier);
556 await setVault($, await getVault($));
557 await setCounts($, await getCounts($));
558 await setRestored($, await getRestored($));
559 await setAllow($, await getAllow($));
560 await setDeny($, await getDeny($));
561 if ((await getDisabled($)) === undefined) await setDisabled($, [...cfg.disabledRules]);
562}
563
564async function scrubText(
565 $: any,
566 text: string,
567 cfg: Cfg,
568 sourcing: Sourcing,
569): Promise<{ enabled: boolean; text: string; hits: readonly ScrubHit[] }> {
570 const { enabled, tier, salt } = await posture($, cfg);
571 if (!enabled || !salt) return { enabled: false, text, hits: [] as const };
572 const result = scrub(text, await optionsFor($, salt, tier, cfg.minEntropy, cfg.budgetMs, cfg.disabledRules, sourcing));
573 return { enabled: true, text: result.text, hits: result.hits };
574}
575
576export function register(on: any, options: Record<string, unknown>): void {
577 const configuredEnabled = options.enabled !== false;
578 const configuredTier = (TIERS.includes(options.tier as Tier) ? options.tier : "balanced") as Tier;
579 const configuredEntropy = typeof options.minEntropy === "number" ? options.minEntropy : 3.2;
580 const configuredBudget = typeof options.scanBudgetMs === "number" ? options.scanBudgetMs : 1500;
581 // `disabledRules` arrives from the manifest as "a,b,c"; the live list is session
582 // state, seeded from this once so `/redact rule on` can put a rule back.
583 const configuredDisabled =
584 typeof options.disabledRules === "string"
585 ? options.disabledRules
586 .split(",")
587 .map((id) => id.trim())
588 .filter((id) => id.length > 0)
589 : [];
590
591 // Helpers that take `$` are module-level declarations (the engine's static
592 // analysis requires it), and `cfg` is passed explicitly rather than closed over.
593 const cfg: Cfg = {
594 enabled: configuredEnabled,
595 tier: configuredTier,
596 minEntropy: configuredEntropy,
597 budgetMs: configuredBudget,
598 disabledRules: configuredDisabled,
599 };
600
601 // A policy mod that seats itself outermost (managed settings, Team and
602 // Enterprise organizations) forwards these events past the user tier. On that
603 // machine the three hooks below can never run, and registering them makes
604 // Claude Code announce a skipped hook for each on every session. A deployment
605 // behind a guard opts out of them at load; with the option unset they stay,
606 // because on a machine with no guard they carry the opaque notice and the
607 // attachment scan.
608 const guardedDeployment = options.guardedDeployment === true;
609
610 // --- session.start: initial readings, capability probe, command ------------
611 on("session.start", async ($: any, e: any, next: (e: any) => unknown) => {
612 // `next(e)` resolves to the session's `{ cwd }`, and this event's hook must
613 // echo that shape back. Returning `undefined` made the engine log "returned
614 // the wrong shape (no result)" and skip the answer in a live session.
615 const answer = (await next(e)) as { cwd?: string } | undefined;
616 try {
617 await initialized($, cfg);
618 } catch {
619 // never fail a session start over state
620 }
621
622 let probed = "built-in 220-rule corpus";
623 // Anything the person configured is worth announcing; the shipped baseline
624 // is not. A clean session should start silently -- an unconditional banner
625 // on every start is noise, and `/redact` is the on-demand answer.
626 let custom = false;
627 try {
628 const found = await localRules($);
629 if (found && found.length > 0) {
630 probed = `built-in corpus + ${found.length} local rule(s)`;
631 custom = true;
632 }
633 } catch {
634 probed = "built-in 220-rule corpus";
635 }
636 try {
637 const model = await localClassifier($);
638 if (model) {
639 probed += ` + L2 local classifier (threshold ${model.threshold})`;
640 custom = true;
641 }
642 } catch {
643 // the ladder simply stays at L1
644 }
645
646 try {
647 await $.command.register({
648 name: "redact",
649 description: "Inspect and control prompt-redact: /redact [stats|on|off|tier|allow|deny|why|help]",
650 });
651 } catch {
652 // a duplicate registration on hot reload is not fatal
653 }
654 if (custom) {
655 try {
656 $.ui.log(`prompt-redact ready — ${probed}`);
657 } catch {
658 // drawing is best-effort
659 }
660 }
661 return answer ?? { cwd: String(e.cwd ?? "") };
662 });
663
664 // --- prompt.submit: the typed prompt, before it is queued ------------------
665 on("prompt.submit", async ($: any, e: any, next: (e: any) => unknown) => {
666 await markObserved($, "prompt.submit");
667 try {
668 const result = await scrubText($, String(e.text ?? ""), cfg, sourcingFor(next, true));
669 if (!result.enabled || result.hits.length === 0) return next(e);
670 await recordHits($, result.hits);
671 const labels = result.hits.map((hit) => hit.label).join(", ");
672 try {
673 $.ui.log(`prompt-redact hid ${result.hits.length} value(s) in this prompt: ${labels}`);
674 } catch {
675 // best-effort
676 }
677 // Context has to ride on the way *down*: an entry added after `next` has
678 // resolved is not attached, the prompt already entered. This is the one
679 // delivery channel that cannot be short-circuited by another plugin, so
680 // the model learns what it is looking at on the very turn it appears.
681 const existing = Array.isArray(e.context) ? (e.context as readonly string[]) : [];
682 const note =
683 `prompt-redact hid ${result.hits.length} value(s) in this message (${labels}). ` +
684 `They appear below as opaque [REDACTED:<LABEL>#<hash>] placeholders, and they came from the user's own ` +
685 `message, so stop rather than work around them: do not guess, decode or reconstruct the value, and do not ` +
686 `ask the user to paste it into the conversation. Tell them it was redacted before it reached you, then give ` +
687 `them the route that works: keep the secret in a file (for example .env) and load it with ` +
688 `\`set -a; . ./.env; set +a\`, or refer to it by variable name. Copy a placeholder verbatim into ` +
689 `Edit/Write/NotebookEdit arguments only when the user wants the value written somewhere: prompt-redact ` +
690 `restores the real value on the way to disk.`;
691 return next({ ...e, text: result.text, context: [...existing, note] });
692 } catch {
693 // Fail closed. A dropped prompt is visible and recoverable; a leaked key is
694 // neither, so an unscannable prompt must not be sent.
695 try {
696 $.ui.log("prompt-redact could not scan this prompt; it was withheld rather than sent unscanned.");
697 } catch {
698 // best-effort
699 }
700 return { drop: "prompt-redact withheld this prompt: the redactor failed while scanning it" };
701 }
702 }).catch((_$: any, _e: any, _next: any) => ({
703 // Engine-level failure (timeout, misreturn, worker loss) is still a leak
704 // risk: fail closed the way the in-code path does.
705 drop: "prompt-redact withheld this prompt: the redactor failed or overran its budget",
706 }));
707
708 // --- session.append: every stored row this tier can see --------------------
709 // Doors: prompt, command, response, tool-result, tool-message, delivery,
710 // attachment, hook-context, note, compaction, notice, queue. This is the one
711 // event that sees a row before BOTH the model and the transcript file, so it
712 // closes the echo-back leak where the model restates a secret in its reply.
713 //
714 // Only doors a user-tier mod can actually read are matched. On an organization
715 // account the guard forwards the injected doors (attachment, hook-context,
716 // compaction), skill rows and org notes past this tier, so registering them
717 // would add a hook that is skipped on every session and print a skip notice per
718 // event for rows this mod can never see. Narrowing the matcher keeps the
719 // transcript quiet and loses no carrier. The exclusion is unconditional: a
720 // deployment seated above the guard would still not see these doors, because
721 // no registration names them.
722 const VISIBLE_DOORS = ["prompt", "command", "response", "tool-result", "delivery", "notice", "queue"] as const;
723 for (const door of VISIBLE_DOORS) {
724 on("session.append", { door }, async ($: any, e: any, next: (e: any) => unknown) => {
725 await markObserved($, "session.append");
726 try {
727 const { enabled, tier, salt } = await posture($, cfg);
728 if (!enabled || !salt) return next(e);
729 const result = scrubMessage(
730 e.message,
731 await optionsFor($, salt, tier, cfg.minEntropy, cfg.budgetMs, cfg.disabledRules, sourcingFor(next, String(e.door ?? "") === "prompt")),
732 );
733 if (result.hits.length === 0) return next(e);
734 await recordHits($, result.hits);
735 return next({ ...e, message: result.message });
736 } catch {
737 // Only doors a person's content travels arrive here, so a failed scan is a
738 // leak risk, never managed content: fail closed.
739 return next({ ...e, message: { ...e.message, content: [{ type: "text", text: WITHHELD }] } });
740 }
741 }).catch(async ($: any, e: any, next: any) => {
742 // The engine's own failure paths (timeout, overrun, worker loss) land here.
743 // Same rule: the doors reaching this mod carry person content, so fail closed.
744 return next({ ...e, message: { ...(e?.message ?? {}), content: [{ type: "text", text: WITHHELD }] } });
745 });
746 }
747
748 if (!guardedDeployment) {
749 // --- prompt.attachment: every message the engine injects on its own ------
750 // This is the event that sees an injected message as a request is about to
751 // carry it, and `e.text` is exactly "what the model reads".
752 //
753 // It matters because a settings hook's additional context arrives here with
754 // `origin.kind === "hook"` (SessionStart, UserPromptSubmit, PostToolUse). A
755 // live session showed that context carrying a raw canary straight into the
756 // request body's system channel: `session.append` never sees it, because it is
757 // not a conversation row, and no hook in this plugin was registered for it.
758 // Redacting it here is the only place that reaches it.
759 on("prompt.attachment", async ($: any, e: any, next: (e: any) => unknown) => {
760 await markObserved($, "prompt.attachment");
761 try {
762 const { enabled, tier, salt } = await posture($, cfg);
763 const got = (await next(e)) as { text: string | null } | undefined;
764 const text = got?.text ?? e.text;
765 if (!enabled || !salt || typeof text !== "string") {
766 return got ?? { text: typeof text === "string" ? text : null };
767 }
768 if (text === "") return { text };
769 const result = scrub(text, await optionsFor($, salt, tier, cfg.minEntropy, cfg.budgetMs, cfg.disabledRules, sourcingFor(next, false)));
770 if (result.hits.length === 0) return { text };
771 await recordHits($, result.hits);
772 return { text: result.text };
773 } catch {
774 // Fail closed: leaving one attachment out is recoverable; carrying it raw
775 // into the request is not. Say so, rather than dropping it quietly.
776 try {
777 $.ui.log("prompt-redact could not scan an attachment; it was withheld rather than carried raw.");
778 } catch {
779 // best-effort
780 }
781 return { text: null };
782 }
783 }).catch(async ($: any, _e: any, _next: any) => {
784 try {
785 $.ui.log("prompt-redact could not scan an attachment; it was withheld rather than carried raw.");
786 } catch {
787 // best-effort
788 }
789 return { text: null };
790 });
791
792 // --- prompt.compose: the standing notice, on the system-prompt surface -----
793 // `prompt.section` was skipped in a live session because another plugin's hook
794 // ran first and passed the event on without calling `next`. `prompt.compose`
795 // is the composition of the whole system prompt, so appending a `session`
796 // section here delivers the notice when the per-section chain is reachable.
797 // Both are registered because the two can be pre-empted independently: on an
798 // organization account the guard forwards each past the user tier, and a
799 // `prompt.section` run that lands while `prompt.compose` is skipped still
800 // delivers it. Whichever lands first sets the state flag, so the notice is not
801 // duplicated when both fire.
802 on("prompt.compose", async ($: any, e: any, next: (e: any) => unknown) => {
803 await markObserved($, "prompt.compose");
804 const got = (await next(e)) as
805 | { sections?: readonly { id: string; text: string; scope: "shared" | "session" }[] }
806 | undefined;
807 if (!got || !Array.isArray(got.sections)) return got;
808 const { enabled } = await posture($, cfg);
809 if (!enabled) return got;
810 if (got.sections.some((section) => section.id === NOTICE_SECTION_ID)) return got;
811 // One notice per prompt: `prompt.section` and `prompt.compose` both deliver
812 // it when both fire, so the second one stands down.
813 if (await getNoticeShown($)) return got;
814 await setNoticeShown($, true);
815 return {
816 sections: [...got.sections, { id: NOTICE_SECTION_ID, text: OPAQUE_NOTICE.trim(), scope: "session" as const }],
817 };
818 });
819
820 // --- prompt.section: tell the model the placeholders are opaque -----------
821 on("prompt.section", async ($: any, e: any, next: (e: any) => unknown) => {
822 // The event reaching this handler is the evidence, even for a section this
823 // mod does not rewrite.
824 await markObserved($, "prompt.section");
825 if (e.name !== "env_info_simple") return next(e);
826 const { enabled } = await posture($, cfg);
827 if (!enabled) return next(e);
828 const got = (await next(e)) as { text: string | null } | undefined;
829 const text = got?.text ?? e.text;
830 if (typeof text !== "string") return got ?? { text: null };
831 if (await getNoticeShown($)) return { text };
832 await setNoticeShown($, true);
833 return { text: text + OPAQUE_NOTICE };
834 });
835 }
836
837 // --- tool.call: rehydrate ONLY the three file-editing tools ---------------
838 for (const tool of REHYDRATE_TOOLS) {
839 on("tool.call", { tool }, async ($: any, e: any, next: (e: any) => unknown) => {
840 await markObserved($, "tool.call");
841 const { enabled } = await posture($, cfg);
842 if (!enabled) return next(e);
843 const vault = await getVault($);
844 if (Object.keys(vault).length === 0) return next(e);
845 const rewritten: Record<string, unknown> = {};
846 let restored = 0;
847 for (const [key, value] of Object.entries(e as Record<string, unknown>)) {
848 if (RESERVED.has(key)) {
849 rewritten[key] = value;
850 continue;
851 }
852 const parsed = asRehydratable(value);
853 if (parsed === undefined) {
854 rewritten[key] = value;
855 continue;
856 }
857 // Count what is about to be restored, before restoring it.
858 restored += placeholdersIn(parsed);
859 rewritten[key] = rehydrate(parsed, vault);
860 }
861 if (restored > 0) await setRestored($, (await getRestored($)) + restored);
862 return next(rewritten);
863 });
864 }
865
866 // --- tool.call: refuse a placeholder on tools that never receive real values
867 // Bash and the network tools are absent from REHYDRATE_TOOLS on purpose: that
868 // absence is what stops a model-written command or URL from becoming an
869 // exfiltration path. The cost is that a placeholder reaches them as literal
870 // text, so a command that "works" silently does nothing. Refusing is the honest
871 // move, and the refusal carries the route that does work.
872 for (const tool of GUARDED_TOOLS) {
873 on("tool.call", { tool }, async ($: any, e: any, next: (e: any) => unknown) => {
874 await markObserved($, "tool.call");
875 const { enabled } = await posture($, cfg);
876 if (!enabled) return next(e);
877 const vault = await getVault($);
878 // No vault means no placeholder of ours is in play: nothing to refuse.
879 if (Object.keys(vault).length === 0) return next(e);
880 let carried = 0;
881 for (const [key, value] of Object.entries(e as Record<string, unknown>)) {
882 if (RESERVED.has(key)) continue;
883 const parsed = asRehydratable(value);
884 if (parsed === undefined) continue;
885 carried += placeholdersIn(parsed);
886 }
887 if (carried === 0) return next(e);
888 return {
889 deny:
890 `prompt-redact: this ${tool} call carries ${carried} redacted placeholder(s). ` +
891 `${tool} never receives real values — that rule is what keeps a model-written command or URL from ` +
892 `becoming an exfiltration path — so this call would have run with [REDACTED:...] as literal text, ` +
893 `not as the secret. Nothing was executed. Use the value without typing it: keep the secret in a file ` +
894 `and load it (\`set -a; . ./.env; set +a\`) then reference $NAME, or ask the user where the value ` +
895 `should be supplied. Do not ask them to paste it into the conversation.`,
896 };
897 });
898 }
899
900 // --- command.run: /redact -------------------------------------------------
901 on("command.run", { command: "redact" }, async ($: any, e: any) => {
902 const args = String(e.args ?? "").trim();
903 const [sub, ...rest] = args.split(/\s+/);
904 const counts = await getCounts($);
905 const restored = await getRestored($);
906 const enabled = (await getEnabled($)) ?? configuredEnabled;
907 const tier = (await getTier($)) ?? configuredTier;
908 const vault = await getVault($);
909 const allow = await getAllow($);
910 const deny = await getDeny($);
911 const disabled = (await getDisabled($)) ?? [...cfg.disabledRules];
912 const fires = await getFires($);
913
914 switch (sub) {
915 case "":
916 case "stats":
917 return { text: report(counts, restored, enabled, tier, Object.keys(vault).length) };
918 case "on":
919 await setEnabled($, true);
920 return { text: "prompt-redact on." };
921 case "off":
922 await setEnabled($, false);
923 return {
924 text:
925 "prompt-redact OFF for this session. Nothing is scanned: secrets in prompts, " +
926 "tool results and replies will reach the model and the transcript file as typed. " +
927 "`/redact on` restores it.",
928 };
929 case "tier": {
930 const wanted = rest[0] as Tier;
931 if (!TIERS.includes(wanted)) {
932 return { text: `usage: /redact tier <${TIERS.join("|")}> — strict redacts most, precision only clear-cut secrets` };
933 }
934 await setTier($, wanted);
935 return { text: `prompt-redact tier set to ${wanted}.` };
936 }
937 case "allow": {
938 const pattern = rest.join(" ");
939 if (!pattern) return { text: "usage: /redact allow <regex> — values matching are never redacted" };
940 if (!compile(pattern)) return { text: `not a valid regex: ${pattern}` };
941 await setAllow($, [...allow, pattern]);
942 return { text: `allowlisted: ${pattern}` };
943 }
944 case "deny": {
945 const pattern = rest.join(" ");
946 if (!pattern) return { text: "usage: /redact deny <regex> — values matching are always redacted" };
947 if (!compile(pattern)) return { text: `not a valid regex: ${pattern}` };
948 await setDeny($, [...deny, pattern]);
949 return { text: `force-redact: ${pattern}` };
950 }
951 case "rule": {
952 // The "remove rules" half of L3. Adding a rule has always been possible
953 // through `.claude/prompt-redact.rules`; until now there was no way to
954 // switch a shipped rule off, which left a person stuck with a noisy rule
955 // and no recourse but disabling the whole mod.
956 const action = rest[0];
957 const ids = rest.slice(1);
958 if (action === undefined || action === "list") {
959 return {
960 text:
961 disabled.length === 0
962 ? "no rules disabled. /redact rule off <id> switches one off; ids are listed in RULES.md."
963 : `disabled (${disabled.length}): ${disabled.join(", ")}`,
964 };
965 }
966 if (action !== "on" && action !== "off") {
967 return { text: "usage: /redact rule on|off <id> [<id>...] — ids are listed in RULES.md" };
968 }
969 if (ids.length === 0) {
970 return { text: `usage: /redact rule ${action} <id> — ids are listed in RULES.md` };
971 }
972 // Validate before writing: a typo must not silently disable nothing.
973 const known = new Set<string>([...RULES.map((rule) => rule.id), ...ENGINE_RULE_IDS]);
974 const unknown = ids.filter((id) => !known.has(id));
975 if (unknown.length > 0) {
976 return { text: `no such rule: ${unknown.join(", ")} — ids are listed in RULES.md` };
977 }
978 const next =
979 action === "off"
980 ? [...new Set([...disabled, ...ids])]
981 : disabled.filter((id) => !ids.includes(id));
982 await setDisabled($, next);
983 return {
984 text: `${action === "off" ? "disabled" : "re-enabled"}: ${ids.join(", ")}\nnow disabled: ${
985 next.length === 0 ? "none" : next.join(", ")
986 }`,
987 };
988 }
989 case "why": {
990 return {
991 text: [
992 "How a value becomes a placeholder:",
993 " 1. structural detectors (PEM private keys, JWTs, IBANs, Luhn card numbers, SSA numbers) — self-proving, no entropy gate.",
994 ` 2. the generated rule corpus, prefiltered by keyword — ${detectorsFor(4096)}.`,
995 " 3. the allowlist — benign high-entropy shapes (git SHAs, UUIDs, checksums, public keys, placeholders) are left alone.",
996 " 4. a context score for the generic family only.",
997 "",
998 `tier ${tier}: strict redacts most, balanced is the default, precision only clear-cut secrets.`,
999 `user allowlist entries: ${allow.length}; force-redact entries: ${deny.length}`,
1000 `rules switched off: ${disabled.length === 0 ? "none" : `${disabled.length} (${disabled.join(", ")})`}`,
1001 `labels hidden this session: ${Object.keys(counts).length}`,
1002 ...interceptionPosture(fires),
1003 "No network, no subprocess, no model call: a detector never transmits the value it hides.",
1004 ].join("\n"),
1005 };
1006 }
1007 case "help":
1008 default:
1009 return {
1010 text: [
1011 "/redact counts by label, restore count, vault size",
1012 "/redact on|off enable or disable scanning for this session",
1013 `/redact tier <${TIERS.join("|")}>`,
1014 "/redact allow <re> stop redacting values matching <re>",
1015 "/redact deny <re> always redact values matching <re>",
1016 "/redact rule on|off <id> switch a shipped rule off or back on",
1017 "/redact rule list which rules are off",
1018 "/redact why how detection works, and this session's posture",
1019 "/redact help this text",
1020 ].join("\n"),
1021 };
1022 }
1023 });
1024
1025 // --- tool.call (every tool): redact the tool's own result -----------------
1026 // `session.append` rewrites `message.content`, but the transcript ALSO stores
1027 // the tool's structured result as a sidecar (`toolUseResult`) outside that
1028 // field. A live run showed the model reading a redacted row while the sidecar
1029 // kept the raw file content. Rewriting the result here is what makes the
1030 // sidecar match what the model reads, because the engine records a hook's
1031 // returned result in the transcript as the tool's result.
1032 on("tool.call", async ($: any, e: any, next: (e: any) => unknown) => {
1033 await markObserved($, "tool.call");
1034 const answer = await next(e);
1035 const { enabled, tier, salt } = await posture($, cfg);
1036 if (!enabled || !salt) return answer;
1037 if (!answer || typeof answer !== "object" || !("result" in (answer as Record<string, unknown>))) {
1038 return answer;
1039 }
1040 try {
1041 const opts = await optionsFor($, salt, tier, cfg.minEntropy, cfg.budgetMs, cfg.disabledRules, sourcingFor(next, true));
1042 const hits: ScrubHit[] = [];
1043 // A tool's result is JSON-shaped; `asRehydratable` is the boundary parse,
1044 // so the walk below is typed end to end instead of threading `unknown`.
1045 const scrubNode = (node: Rehydratable): Rehydratable => {
1046 if (typeof node === "string") {
1047 const result = scrub(node, opts);
1048 if (result.hits.length > 0) hits.push(...result.hits);
1049 return result.text;
1050 }
1051 if (Array.isArray(node)) return [...node].map(scrubNode);
1052 if (node !== null && typeof node === "object") {
1053 const out: Record<string, Rehydratable> = {};
1054 for (const [key, child] of Object.entries(node)) out[key] = scrubNode(child);
1055 return out;
1056 }
1057 return node;
1058 };
1059 const rewritten = { ...(answer as Record<string, unknown>) };
1060 // Parse each field at its boundary. A field that is not JSON-shaped (a
1061 // Buffer, a function) has no parsed form, so it is left alone for the
1062 // row-level pass rather than coerced into one.
1063 for (const field of ["text", "result"] as const) {
1064 if (!(field in rewritten)) continue;
1065 const parsed = asRehydratable(rewritten[field]);
1066 if (parsed !== undefined) rewritten[field] = scrubNode(parsed);
1067 }
1068 if (hits.length > 0) await recordHits($, hits);
1069 return rewritten;
1070 } catch {
1071 // Fail closed: the tool ran, but its raw output must not reach the model
1072 // or the transcript. An error result is recoverable; a leak is not.
1073 return { deny: "prompt-redact could not scan this tool result; it was withheld rather than returned raw" };
1074 }
1075 }).catch(() => ({
1076 // Engine-level failure: the tool ran, but its output must not reach the model raw.
1077 deny: "prompt-redact could not scan this tool result; it was withheld rather than returned raw",
1078 }));
1079
1080 // --- ui.render: the transparent band above the prompt ---------------------
1081 // Draws only when there is something to say. Elements come from the surface's
1082 // own constructors (`$.ui.resolve`), never hand-built plain objects: a tree
1083 // that does not validate draws the engine's own component instead, silently.
1084 on("ui.render", { component: "AbovePrompt" }, async ($: any, e: any, next: (e: any) => unknown) => {
1085 const counts = await getCounts($);
1086 const enabled = (await getEnabled($)) ?? configuredEnabled;
1087 const total = Object.values(counts).reduce((n, count) => n + count, 0);
1088 if (enabled && total === 0) return next(e);
1089 const labels = Object.entries(counts)
1090 .sort((a, b) => b[1] - a[1])
1091 .slice(0, 4)
1092 .map(([label, count]) => `${label} x${count}`);
1093 const line = enabled
1094 ? `\u{1F6E1} prompt-redact hid ${total}${labels.length ? ` \u2014 ${labels.join(" \u00b7 ")}` : ""}`
1095 : `\u{1F6E1} prompt-redact OFF${total > 0 ? ` (${total} hidden earlier)` : ""}`;
1096 try {
1097 const { Box, Text } = $.ui.resolve(e);
1098 return Box({ paddingLeft: 1 }, Text({ dimColor: true }, line));
1099 } catch {
1100 // A surface with no drawable element set: say nothing rather than draw wrong.
1101 return next(e);
1102 }
1103 });
1104}
1105hooks/scrub.ts 700 lines1// The scrub engine: pure functions, no `$`, no I/O. register.ts owns state; this
2// file owns the decision "is this span a secret, and what does it become".
3//
4// Shape of a decision, in order:
5//
6// 1. structural detectors (PEM, JWT, IBAN, Luhn, SSA) -- self-proving, so they
7// are accepted without an entropy gate and always win span conflicts.
8// 2. the generated rule corpus, prefiltered by keyword, bounded quantifiers.
9// The captured value is chosen as the *highest-entropy group* rather than a
10// fixed group number: gitleaks' own rules disagree (in `atlassian-api-token`
11// group 1 is the vendor name, group 2 is the secret).
12// 3. the allowlist, which is what lets the generic detector run at a low
13// threshold: benign high-entropy shapes are named explicitly and left alone,
14// so the aggression goes where the secrets are.
15// 4. a context score for the generic family only -- the family whose precision
16// depends on surrounding words.
17//
18// Placeholders are deterministic: fnv1a(salt + value) twice, 16 hex. Same secret
19// -> same placeholder in the prompt row, the tool-result row, an edit, and a
20// compaction summary, so an edit to a line that holds a real key still matches.
21// A placeholder minted in another session's salt means nothing here.
22
23import {
24 GLOBAL_ALLOW_REGEXES,
25 GLOBAL_STOPWORDS,
26 PASSWORD_VALUE_RULE_ID,
27 RULES,
28 type Rule,
29} from "./rules.ts";
30import { isAllowed } from "./allowlist.ts";
31import { isSecret, type ClassifierModel } from "./classifier.ts";
32import { structuralHits, type Hit } from "./structural.ts";
33
34export type Tier = "strict" | "balanced" | "precision";
35
36export type ScrubOptions = {
37 /** Per-session salt. A placeholder from another session is meaningless. */
38 readonly salt: string;
39 readonly tier: Tier;
40 /** Entropy floor for the generic family, after a user override. */
41 readonly minEntropy: number;
42 /** User-supplied rules merged ahead of the corpus. */
43 readonly extraRules?: readonly Rule[];
44 /** User allowlist patterns, matched against the whole candidate value. */
45 readonly allowExtra?: readonly RegExp[];
46 /** User force-redact patterns: a match here bypasses every gate. */
47 readonly deny?: readonly RegExp[];
48 /**
49 * Rule ids switched off. The "remove" half of the override surface: a person
50 * who finds a rule noisy must be able to turn it off without forking the
51 * corpus. `local-classifier` disables the optional L2 stage.
52 */
53 readonly disabledRules?: readonly string[];
54 /** Wall-clock ceiling for one row; exceeding it fails closed. */
55 readonly budgetMs?: number;
56 /**
57 * L2: an optional local model, probed from disk by the caller (register.ts).
58 *
59 * Absent or null means the pipeline is L1 only, which is the default and the
60 * normal case. A model here is a weights file evaluated in-process; it never
61 * implies a network, a subprocess or a remote model.
62 */
63 readonly classifier?: ClassifierModel | null;
64};
65
66export type ScrubHit = {
67 readonly label: string;
68 readonly placeholder: string;
69 readonly value: string;
70 readonly by: string;
71};
72
73export type ScrubResult = {
74 readonly text: string;
75 readonly hits: readonly ScrubHit[];
76};
77
78/** Tier thresholds for the generic family's context score, in [0,1]. */
79const TIER_THRESHOLD: Record<Tier, number> = {
80 // A redactor's false positive costs a degraded-looking hash, not a lost task,
81 // so the operating point sits far below a scanner's.
82 strict: 0.15,
83 balanced: 0.35,
84 precision: 0.6,
85};
86
87/** Rows above this are scanned with the structural and generic families only. */
88const FULL_SCAN_LIMIT = 60_000;
89
90/**
91 * Hard ceiling on placeholders minted from one row.
92 *
93 * Exceeding it fails closed. Accepting the first 512 candidates and returning
94 * the text as though it were scrubbed would leave the rest of the row raw while
95 * reporting success -- a partial scrub that reads as a complete one, which is
96 * the worst outcome available to a redactor. A withheld row is visible and
97 * recoverable; a half-scrubbed row is neither.
98 */
99const MAX_HITS = 512;
100
101/**
102 * A run long enough to hold a credential, for the L2 sweep. The class is the
103 * union of what base64, base64url, hex, dashed and dotted tokens use, so one
104 * pass reaches every shape a model could judge without a vendor anchor.
105 */
106const TOKEN_RE = /[A-Za-z0-9_+/=.-]{12,4096}/g;
107
108const GLOBAL_ALLOW = GLOBAL_ALLOW_REGEXES.map((s) => {
109 try {
110 return new RegExp(s, "i");
111 } catch {
112 return null;
113 }
114}).filter((r): r is RegExp => r !== null);
115
116/**
117 * A field-name assignment carrying a password a person may have chosen.
118 *
119 * The field name is the anchor: `password`, `passwd`, `pwd`, `pass` or `secret`,
120 * optionally behind `DB_`/`db.`-style qualifiers, with the separator abutting the
121 * name (`KEY=value`, `key: value`). The value is 6-20 characters and is not
122 * judged by entropy: `S3cr3tP4ssw0rd` measures 3.38 bits and is exactly the
123 * ordinary password the generic rule misses.
124 *
125 * Whitespace before the separator is deliberately not matched: `password =
126 * "hunter2"` is a prose-shaped mention, not a `KEY=value` assignment. The
127 * allowlist gates every candidate, so `changeme`, templates and `required`
128 * survive byte-identical.
129 */
130const PASSWORD_VALUE_RE =
131 /(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,30}[_.-]){0,6}(?:password|passwd|pwd|pass|secret)(?::=|=>|:|=)[ \t]{0,4}(["']?)([^\s"'`<>;,]{6,20})\1(?=$|[\s"'`<>,;)\]}])/gdi;
132
133/** Entropy-free detectors whose rule id L3 can switch off; not in the corpus. */
134const ENGINE_RULE_IDS = new Set<string>([PASSWORD_VALUE_RULE_ID]);
135
136/**
137 * True when a candidate is a filesystem path, not a password.
138 *
139 * `pwd` and `PWD` belong to the field-name keyword set, and a shell environment
140 * dump carries a working directory beside every real secret. A path has nothing
141 * to revoke and is needed verbatim to run the task. A value containing a colon
142 * is not a bare path (it carries a scheme, drive or port), so the slash clause
143 * stays narrow.
144 */
145function isPathValue(value: string): boolean {
146 if (value.startsWith("/") || value.startsWith("~") || value.startsWith("./") || value.startsWith("../")) {
147 return true;
148 }
149 return value.includes("/") && !value.includes(":");
150}
151
152// --- primitives -------------------------------------------------------------
153
154/** Shannon entropy in bits per character, over the value's own distribution. */
155export function shannon(s: string): number {
156 if (s.length === 0) return 0;
157 const counts = new Map<string, number>();
158 for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
159 let h = 0;
160 for (const count of counts.values()) {
161 const p = count / s.length;
162 h -= p * Math.log2(p);
163 }
164 return h;
165}
166
167function fnv1a(input: string, basis: number): string {
168 let h = basis >>> 0;
169 for (let i = 0; i < input.length; i++) {
170 h ^= input.charCodeAt(i);
171 h = Math.imul(h, 0x01000193) >>> 0;
172 }
173 return (h >>> 0).toString(16).padStart(8, "0");
174}
175
176/** 64 bits from two independent FNV-1a passes: collisions are not a concern. */
177function valueHash(salt: string, value: string): string {
178 return fnv1a(salt + value, 0x811c9dc5) + fnv1a(value + salt, 0x7a3d1f29);
179}
180
181const PLACEHOLDER_RE = /^\[REDACTED:[A-Z0-9_]+#[0-9a-f]{16}\]$/;
182
183/** True for a value that is already one of our placeholders (idempotence). */
184export function isPlaceholder(s: string): boolean {
185 return PLACEHOLDER_RE.test(s.trim());
186}
187
188export function placeholderFor(label: string, salt: string, value: string): string {
189 return `[REDACTED:${label}#${valueHash(salt, value)}]`;
190}
191
192const PROSE_PLACEHOLDER_RE = /\[REDACTED:[A-Z0-9_]+#[0-9a-f]{16}\]/g;
193
194// --- context scoring -------------------------------------------------------
195
196const BOOST = ["prod", "production", "live", "deploy", "secret", "token", "credential", "password", "private", "auth", "bearer", "apikey", "api_key"];
197const DAMP = ["example", "sample", "dummy", "fake", "fixture", "mock", "test", "docs", "readme", "placeholder", "lorem"];
198
199/**
200 * Confidence that a generic-family candidate is a real credential, in [0,1].
201 *
202 * Deliberately conservative in what it *suppresses*: a damping word lowers the
203 * score but the explicit allowlist is what actually lets a value through, so a
204 * test fixture still gets redacted unless it is one of the named benign shapes.
205 */
206export function contextScore(text: string, start: number, end: number, value: string): number {
207 let score = 0.45;
208 const window = text.slice(Math.max(0, start - 80), Math.min(text.length, end + 40)).toLowerCase();
209 for (const word of BOOST) if (window.includes(word)) score += 0.12;
210 for (const word of DAMP) if (window.includes(word)) score -= 0.1;
211 // A value that mixes character classes is much more likely to be generated.
212 const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((re) => re.test(value)).length;
213 score += (classes - 1) * 0.08;
214 if (value.length >= 32) score += 0.1;
215 if (value.length >= 64) score += 0.05;
216 return Math.max(0, Math.min(1, score));
217}
218
219// --- rule matching ---------------------------------------------------------
220
221type Compiled = { rule: Rule; re: RegExp | null };
222
223/**
224 * Compile once per load. A pattern that fails to compile is dropped rather than
225 * thrown: one bad user rule must not disable the whole redactor.
226 */
227function compile(rules: readonly Rule[]): Compiled[] {
228 return rules.map((rule) => {
229 try {
230 // `d` gives match indices, so only the chosen group's span is redacted.
231 return { rule, re: new RegExp(rule.regex, `${rule.flags}g${rule.flags.includes("d") ? "" : "d"}`) };
232 } catch {
233 return { rule, re: null };
234 }
235 });
236}
237
238/**
239 * The corpus scan. Engine-owned detectors are excluded: they run in their own
240 * passes, where a tier gate and an exact value span replace the generic
241 * family's entropy and context-score gates.
242 */
243const COMPILED = compile(RULES.filter((rule) => !ENGINE_RULE_IDS.has(rule.id)));
244
245/** keyword -> compiled rules, so a row only runs rules whose anchor it mentions. */
246const BY_KEYWORD = new Map<string, Compiled[]>();
247for (const c of COMPILED) {
248 for (const k of c.rule.keywords) {
249 const list = BY_KEYWORD.get(k);
250 if (list) list.push(c);
251 else BY_KEYWORD.set(k, [c]);
252 }
253}
254
255const KEYWORDS = [...BY_KEYWORD.keys()];
256
257function rulesForRow(size: number, extra: readonly Rule[]): Compiled[] {
258 if (extra.length) {
259 return [...compile(extra), ...(size <= FULL_SCAN_LIMIT ? COMPILED : COMPILED.filter((c) => c.rule.kind !== "vendor-keyword"))];
260 }
261 return size <= FULL_SCAN_LIMIT ? COMPILED : COMPILED.filter((c) => c.rule.kind !== "vendor-keyword");
262}
263
264/** Which detectors a row of this size will actually run (`/redact why`). */
265export function detectorsFor(size: number): string {
266 const full = size <= FULL_SCAN_LIMIT;
267 return full
268 ? "structural + 220-rule corpus + generic"
269 : `structural + generic (row is ${size} chars, over the ${FULL_SCAN_LIMIT} full-scan limit)`;
270}
271
272/**
273 * Pick the span to redact from one match.
274 *
275 * gitleaks rules disagree about which capture group holds the value, so rather
276 * than trusting a group number we take the highest-entropy non-empty group and
277 * break ties on length. `secretGroup` wins when upstream states it explicitly.
278 */
279function chosenSpan(m: RegExpExecArray, rule: Rule): { start: number; end: number } | null {
280 const indices = m.indices;
281 if (!indices) return null;
282 const declared = rule.secretGroup;
283 if (declared !== null && declared > 0 && indices[declared]) {
284 const span = indices[declared];
285 return span ? { start: span[0], end: span[1] } : null;
286 }
287 let best: { start: number; end: number } | null = null;
288 let bestScore = -1;
289 let bestLength = -1;
290 for (let i = 1; i < indices.length; i++) {
291 const span = indices[i];
292 if (!span || span[1] <= span[0]) continue;
293 const value = m[0].slice(span[0] - (m.index ?? 0), span[1] - (m.index ?? 0));
294 const score = shannon(value);
295 const length = span[1] - span[0];
296 if (score > bestScore || (score === bestScore && length > bestLength)) {
297 best = { start: span[0], end: span[1] };
298 bestScore = score;
299 bestLength = length;
300 }
301 }
302 if (best) return best;
303 const whole = indices[0];
304 return whole ? { start: whole[0], end: whole[1] } : null;
305}
306
307type Candidate = Hit & { by: string };
308
309/**
310 * Scan one string. Returns the rewritten text and every hit, but mints no state:
311 * the caller decides what to persist.
312 *
313 * Fails closed by throwing, never by returning partially-scrubbed text: a row
314 * that overruns the budget must not reach the model at all.
315 */
316export function scrub(text: string, opts: ScrubOptions): ScrubResult {
317 if (!text) return { text, hits: [] };
318 const started = Date.now();
319 const budget = opts.budgetMs ?? 1500;
320 const threshold = TIER_THRESHOLD[opts.tier];
321 const candidates: Candidate[] = [];
322 const disabled = opts.disabledRules && opts.disabledRules.length > 0 ? new Set(opts.disabledRules) : null;
323
324 // 1. structural: self-proving, so they bypass every gate -- except the L3
325 // remove surface. The URL and Basic-auth detectors carry rule ids, so a
326 // disabled id stops them the same way it stops a rule in step 2 ("a disabled
327 // rule is not consulted, whichever family it belongs to"); the checksum
328 // families have no id a command can name, so they stay unconditional.
329 for (const hit of structuralHits(text)) {
330 if (opts.disabledRules?.includes(hit.by)) continue;
331 candidates.push(hit);
332 }
333
334 // 1b. force-redact patterns are detectors in their own right. A person who
335 // says "always redact this" must get that even when no rule matches the value
336 // -- otherwise the override is silently unreachable for exactly the values
337 // they cared about. `/redact deny <re>` documents value semantics, so the
338 // pattern is tested against each token as well as against the whole text
339 // (the latter lets a pattern span whitespace when someone wants that).
340 const DENY_TOKEN = /[^\s"'`<>()[\]{},;]+/g;
341 for (const pattern of opts.deny ?? []) {
342 const push = (start: number, end: number, value: string) => {
343 candidates.push({ label: "USER_DENY", start, end, value, by: "deny" });
344 };
345 pattern.lastIndex = 0;
346 let scanner: RegExp;
347 try {
348 scanner = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
349 } catch {
350 continue;
351 }
352 scanner.lastIndex = 0;
353 for (;;) {
354 const m = scanner.exec(text);
355 if (!m) break;
356 if (m[0].length === 0) {
357 scanner.lastIndex += 1;
358 continue;
359 }
360 push(m.index, m.index + m[0].length, m[0]);
361 }
362 DENY_TOKEN.lastIndex = 0;
363 for (;;) {
364 const token = DENY_TOKEN.exec(text);
365 if (!token) break;
366 scanner.lastIndex = 0;
367 // Only what the pattern matched inside the token disappears, so the token
368 // pass preserves the surrounding URL or sentence. The whole-text pass is
369 // what lets a pattern span whitespace; this pass adds value-anchored
370 // semantics (`^`/`$` bind to the token, not to the row).
371 for (;;) {
372 const m = scanner.exec(token[0]);
373 if (!m) break;
374 if (m[0].length === 0) {
375 scanner.lastIndex += 1;
376 continue;
377 }
378 push(token.index + m.index, token.index + m.index + m[0].length, m[0]);
379 }
380 }
381 }
382
383 // 1c. password values, keyed on the field name.
384 //
385 // The generic family cannot reach an ordinary password: its 3.5-bit floor is
386 // what separates generated keys from words, and a person's password sits below
387 // it. This pass trades that gate for the allowlist alone, and runs only at the
388 // default tier: `strict` keeps its shipped precision baseline.
389 if (opts.tier === "balanced" && !disabled?.has(PASSWORD_VALUE_RULE_ID)) {
390 PASSWORD_VALUE_RE.lastIndex = 0;
391 for (;;) {
392 const m = PASSWORD_VALUE_RE.exec(text);
393 if (!m) break;
394 if (Date.now() - started > budget) throw new Error("prompt-redact: scan budget exceeded");
395 const span = m.indices?.[2];
396 if (!span) continue;
397 const value = text.slice(span[0], span[1]);
398 if (!value || isPlaceholder(value)) continue;
399 if (isPathValue(value)) continue;
400 if ((opts.deny ?? []).some((re) => re.test(value))) {
401 candidates.push({ label: "PASSWORD_VALUE", start: span[0], end: span[1], value, by: PASSWORD_VALUE_RULE_ID });
402 continue;
403 }
404 if (isAllowed(value, opts.allowExtra ?? [])) continue;
405 if (GLOBAL_ALLOW.some((re) => re.test(value))) continue;
406 if (GLOBAL_STOPWORDS.some((w) => value.toLowerCase() === w)) continue;
407 candidates.push({ label: "PASSWORD_VALUE", start: span[0], end: span[1], value, by: PASSWORD_VALUE_RULE_ID });
408 }
409 }
410
411 // 2. rules, keyword-prefiltered.
412 const lower = text.toLowerCase();
413 const wanted = new Set<Compiled>();
414 for (const keyword of KEYWORDS) {
415 if (lower.includes(keyword)) {
416 for (const c of BY_KEYWORD.get(keyword) ?? []) wanted.add(c);
417 }
418 }
419 for (const c of rulesForRow(text.length, opts.extraRules ?? [])) {
420 if (c.rule === undefined || c.re === null) continue;
421 // A disabled rule is not consulted, whichever family it belongs to.
422 if (disabled?.has(c.rule.id)) continue;
423 if (c.rule.keywords.length > 0 && !wanted.has(c)) continue;
424 // A rule with no keywords runs unconditionally; those are cheap and rare.
425 if (Date.now() - started > budget) throw new Error("prompt-redact: scan budget exceeded");
426 c.re.lastIndex = 0;
427 for (;;) {
428 const m = c.re.exec(text);
429 if (!m) break;
430 if (m[0].length === 0) {
431 c.re.lastIndex += 1;
432 continue;
433 }
434 const span = chosenSpan(m, c.rule);
435 if (!span) continue;
436 const value = text.slice(span.start, span.end);
437 if (!value || isPlaceholder(value)) continue;
438 // A force-redact rule is the person's explicit override and outranks every
439 // other gate: allowlist, stopwords, entropy and context score.
440 if ((opts.deny ?? []).some((re) => re.test(value))) {
441 candidates.push({ label: c.rule.label, start: span.start, end: span.end, value, by: c.rule.id });
442 continue;
443 }
444 if (isAllowed(value, opts.allowExtra ?? [])) continue;
445 if (GLOBAL_ALLOW.some((re) => re.test(value))) continue;
446 if (GLOBAL_STOPWORDS.some((w) => value.toLowerCase() === w)) continue;
447 // `minEntropy` is the floor for the generic family, and it has to be able
448 // to *raise* the rule's own floor as well as lower it. A rule that carries
449 // `entropy: 3.5` upstream does not stop being 3.5 because a person asked
450 // for 2.0; and asking for 4.0 must actually tighten it, or the setting is
451 // decorative. Specific rules keep their upstream value only.
452 const floor =
453 c.rule.kind === "generic"
454 ? Math.max(c.rule.entropy ?? 0, opts.minEntropy)
455 : (c.rule.entropy ?? 0);
456 if (floor > 0 && shannon(value) < floor) continue;
457 if (c.rule.kind === "generic" && contextScore(text, span.start, span.end, value) < threshold) continue;
458 candidates.push({ label: c.rule.label, start: span.start, end: span.end, value, by: c.rule.id });
459 }
460 }
461
462 // 2b. L2: the optional local classifier.
463 //
464 // This is the only detector that runs without an anchor. A rule corpus cannot
465 // reach a secret that carries no vendor prefix and no keyword beside it -- the
466 // prose/multilingual tier -- so when a weights file was probed and loaded the
467 // row is swept for high-entropy runs and each one is scored. Absent the model
468 // this block is skipped entirely and nothing about L1 changes.
469 if (opts.classifier && !disabled?.has("local-classifier")) {
470 const model = opts.classifier;
471 TOKEN_RE.lastIndex = 0;
472 for (;;) {
473 const token = TOKEN_RE.exec(text);
474 if (!token) break;
475 if (Date.now() - started > budget) throw new Error("prompt-redact: scan budget exceeded");
476 const value = token[0];
477 if (isPlaceholder(value)) continue;
478 if (isAllowed(value, opts.allowExtra ?? [])) continue;
479 if (GLOBAL_ALLOW.some((re) => re.test(value))) continue;
480 if (shannon(value) < opts.minEntropy) continue;
481 if (!isSecret(model, value)) continue;
482 candidates.push({
483 label: "LOCAL_CLASSIFIER_SECRET",
484 start: token.index,
485 end: token.index + value.length,
486 value,
487 by: "classifier",
488 });
489 }
490 }
491
492 // 3. resolve overlaps: structural first, then widest/highest-entropy.
493 // Rank decides which of two overlapping candidates keeps its span. Self-proving
494 // evidence outranks a pattern, a pattern outranks a statistical guess: a vendor
495 // rule names the credential, so its label is worth more than the classifier's.
496 const rank = (by: string): number => {
497 if (by === "deny" || by === "pem" || by === "jwt" || by === "luhn" || by === "iban" || by === "fica" || by === "base32") return 2;
498 if (by === "classifier") return 0;
499 return 1;
500 };
501 const ordered = candidates
502 .map((c, i) => ({ c, i }))
503 .sort((a, b) => {
504 const sa = rank(a.c.by);
505 const sb = rank(b.c.by);
506 if (sa !== sb) return sb - sa;
507 if (a.c.end - a.c.start !== b.c.end - b.c.start) return b.c.end - b.c.start - (a.c.end - a.c.start);
508 return shannon(b.c.value) - shannon(a.c.value);
509 });
510
511 const accepted: Candidate[] = [];
512 let overflowed = false;
513 for (const { c } of ordered) {
514 if (accepted.some((a) => c.start < a.end && a.start < c.end)) continue;
515 if (accepted.length >= MAX_HITS) {
516 // Count only candidates that would genuinely have been accepted, so a row
517 // full of overlapping matches is not mistaken for an over-cap row.
518 overflowed = true;
519 break;
520 }
521 accepted.push(c);
522 }
523 if (overflowed) {
524 throw new Error(`prompt-redact: more than ${MAX_HITS} candidate secrets in one row`);
525 }
526
527 // 4. apply right-to-left so earlier spans keep their offsets.
528 accepted.sort((a, b) => a.start - b.start);
529 const hits: ScrubHit[] = [];
530 let out = text;
531 for (let i = accepted.length - 1; i >= 0; i--) {
532 const hit = accepted[i];
533 if (!hit) continue;
534 const placeholder = placeholderFor(hit.label, opts.salt, hit.value);
535 out = out.slice(0, hit.start) + placeholder + out.slice(hit.end);
536 hits.push({ label: hit.label, placeholder, value: hit.value, by: hit.by });
537 }
538 hits.reverse();
539 if (Date.now() - started > budget) throw new Error("prompt-redact: scan budget exceeded");
540 return { text: out, hits };
541}
542
543// --- block walking ---------------------------------------------------------
544
545type Block = { type?: string; [field: string]: unknown };
546type Message = { type?: string; content?: unknown; [field: string]: unknown };
547
548const TEXT_TYPES = new Set(["text"]);
549
550/**
551 * Rewrite every rewritable part of a stored row.
552 *
553 * On `session.append` the engine pins `type`, `name`, `role` and `isMeta`, and
554 * puts back thinking blocks, `tool_use` blocks and every `tool_result.tool_use_id`.
555 * What a hook may change is exactly: text blocks, a `tool_result`'s `content`,
556 * and `is_error`. This function touches nothing else.
557 */
558export function scrubMessage(message: Message, opts: ScrubOptions): { message: Message; hits: ScrubHit[] } {
559 const hits: ScrubHit[] = [];
560 const content = message.content;
561 if (typeof content === "string") {
562 const result = scrub(content, opts);
563 hits.push(...result.hits);
564 return { message: { ...message, content: result.text }, hits };
565 }
566 if (!Array.isArray(content)) return { message, hits };
567
568 const next = content.map((raw) => {
569 const block = raw as Block;
570 if (!block || typeof block !== "object") return raw;
571 if (TEXT_TYPES.has(String(block.type)) && typeof block.text === "string") {
572 const result = scrub(block.text, opts);
573 hits.push(...result.hits);
574 return result.hits.length ? { ...block, text: result.text } : block;
575 }
576 if (block.type === "tool_result") {
577 const inner = block.content;
578 if (typeof inner === "string") {
579 const result = scrub(inner, opts);
580 hits.push(...result.hits);
581 return result.hits.length ? { ...block, content: result.text } : block;
582 }
583 if (Array.isArray(inner)) {
584 const blocks = inner.map((nested) => {
585 const b = nested as Block;
586 if (b && typeof b === "object" && typeof b.text === "string") {
587 const result = scrub(b.text, opts);
588 hits.push(...result.hits);
589 return result.hits.length ? { ...b, text: result.text } : b;
590 }
591 return nested;
592 });
593 return hits.length ? { ...block, content: blocks } : block;
594 }
595 }
596 return block;
597 });
598 return { message: { ...message, content: next }, hits };
599}
600
601// --- rehydration -----------------------------------------------------------
602
603/**
604 * The domain rehydration operates on: JSON-shaped tool input. Declaring it here
605 * rather than threading `unknown` through keeps the walk typed at every level.
606 */
607export type Rehydratable =
608 | string
609 | number
610 | boolean
611 | null
612 | readonly Rehydratable[]
613 | { readonly [key: string]: Rehydratable };
614
615/**
616 * Parse an engine-supplied tool input into the rehydration domain.
617 *
618 * `undefined` means "not rehydratable shape" and the caller keeps the original
619 * value; `null` is the genuine JSON null. Anything a tool can carry that is not
620 * string/array/object (a timeout number, a boolean flag) round-trips untouched.
621 */
622export function asRehydratable(value: unknown): Rehydratable | undefined {
623 if (value === null) return null;
624 switch (typeof value) {
625 case "string":
626 case "number":
627 case "boolean":
628 return value;
629 case "object":
630 break;
631 default:
632 return undefined;
633 }
634 if (Array.isArray(value)) {
635 const items: Rehydratable[] = [];
636 for (const item of value) {
637 const parsed = asRehydratable(item);
638 items.push(parsed === undefined ? String(item) : parsed);
639 }
640 return items;
641 }
642 const record: Record<string, Rehydratable> = {};
643 for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
644 const parsed = asRehydratable(item);
645 if (parsed !== undefined) record[key] = parsed;
646 }
647 return record;
648}
649
650function rehydrateNode(node: Rehydratable, vault: Readonly<Record<string, string>>): Rehydratable {
651 if (typeof node === "string") {
652 let out = node;
653 for (const [placeholder, value] of Object.entries(vault)) {
654 if (out.includes(placeholder)) out = out.split(placeholder).join(value);
655 }
656 return out;
657 }
658 if (Array.isArray(node)) return node.map((child) => rehydrateNode(child, vault));
659 if (node !== null && typeof node === "object") {
660 const out: Record<string, Rehydratable> = {};
661 for (const [key, child] of Object.entries(node)) out[key] = rehydrateNode(child, vault);
662 return out;
663 }
664 return node;
665}
666
667/**
668 * Put real values back. Callers must only ever pass `Edit`, `Write` and
669 * `NotebookEdit` arguments: rehydrating anywhere the model can reach the network
670 * turns a placeholder into an exfiltration primitive.
671 */
672export function rehydrate(input: Rehydratable, vault: Readonly<Record<string, string>>): Rehydratable {
673 if (Object.keys(vault).length === 0) return input;
674 return rehydrateNode(input, vault);
675}
676
677/** Count placeholders inside a structured value (pre-rehydration), for `/redact`. */
678export function placeholdersIn(node: Rehydratable): number {
679 if (typeof node === "string") return placeholderCount(node);
680 // Explicit loops rather than reduce: the accumulator type of `reduce` over a
681 // union of readonly array and record resolves to the element type, not number.
682 if (Array.isArray(node)) {
683 let total = 0;
684 for (const child of node as readonly Rehydratable[]) total += placeholdersIn(child);
685 return total;
686 }
687 if (node !== null && typeof node === "object") {
688 let total = 0;
689 for (const child of Object.values(node) as Rehydratable[]) total += placeholdersIn(child);
690 return total;
691 }
692 return 0;
693}
694
695/** Count placeholders still present in a string, for `/redact` reporting. */
696export function placeholderCount(text: string): number {
697 PROSE_PLACEHOLDER_RE.lastIndex = 0;
698 return (text.match(PROSE_PLACEHOLDER_RE) ?? []).length;
699}
700hooks/classifier.ts 163 lines1// L2: an optional local classifier, probed from disk and run in-process.
2//
3// The rule corpus is precise because every rule carries an anchor: a vendor
4// prefix or a keyword. That is also its ceiling. A password written as prose, or
5// a proprietary token with no vendor prefix, has no anchor for a regex to grip,
6// which is the tier where regex recall collapses (ProwlBench T3).
7//
8// So this module adds the second way to detect: a linear model over features of
9// a candidate value. It is honest about what it is -- a weights file the person
10// (or their trainer) supplies, evaluated with ordinary arithmetic. No weights
11// ship with the mod, and none can: training needs data that leaves the machine,
12// which the no-egress invariant forbids. When the file is absent, unreadable, or
13// malformed, `parseClassifier` returns null and the pipeline silently stays at
14// L1. That fallback is the normal case, not an error case.
15//
16// The invariant still holds. Inference is `bias + sum(w[f] * x[f])`, then a
17// sigmoid. Nothing is transmitted, spawned or delegated: the value being judged
18// never leaves this process, and this file cannot reach the network because it
19// imports nothing and the posture gate forbids the call sites that could.
20
21/** A local linear model: one weight per named feature, plus a bias. */
22export type ClassifierModel = {
23 readonly version: 1;
24 /** Log-odds offset: a positive bias makes the model fire more readily. */
25 readonly bias: number;
26 /** Probability above which a candidate counts as a secret, in (0,1). */
27 readonly threshold: number;
28 /** Feature name -> weight. Names outside `FEATURE_NAMES` are ignored. */
29 readonly features: Readonly<Record<string, number>>;
30};
31
32/**
33 * The feature vocabulary. A weights file may name any subset; an unknown name is
34 * ignored rather than rejected, so a newer trainer's extra features degrade
35 * quietly instead of disabling the model.
36 */
37export const FEATURE_NAMES = [
38 "length",
39 "entropy",
40 "upper",
41 "lower",
42 "digit",
43 "symbol",
44 "hexish",
45 "base64ish",
46 "camel",
47 "separator",
48 "prefixed",
49 "wordlike",
50] as const;
51
52export type FeatureName = (typeof FEATURE_NAMES)[number];
53
54/** Sigmoid input clamp: past this the answer is already 0 or 1 to the bit. */
55const CLAMP = 60;
56
57function finite(value: unknown): value is number {
58 return typeof value === "number" && Number.isFinite(value);
59}
60
61/**
62 * Parse a weights file. Anything malformed yields null -- a broken model must
63 * fall back to L1, never half-load and misjudge every candidate.
64 *
65 * Shape:
66 * { "version": 1, "bias": -3.2, "threshold": 0.5,
67 * "features": { "entropy": 1.4, "digit": 2.1, "symbol": 0.8 } }
68 */
69export function parseClassifier(raw: string): ClassifierModel | null {
70 let parsed: unknown;
71 try {
72 parsed = JSON.parse(raw);
73 } catch {
74 return null;
75 }
76 if (typeof parsed !== "object" || parsed === null) return null;
77 const record = parsed as Record<string, unknown>;
78 if (record.version !== 1) return null;
79 if (!finite(record.bias)) return null;
80 if (!finite(record.threshold) || record.threshold <= 0 || record.threshold >= 1) return null;
81 const rawFeatures = record.features;
82 if (typeof rawFeatures !== "object" || rawFeatures === null) return null;
83 const features: Record<string, number> = {};
84 for (const [name, weight] of Object.entries(rawFeatures as Record<string, unknown>)) {
85 if (!finite(weight)) return null;
86 features[name] = weight;
87 }
88 if (Object.keys(features).length === 0) return null;
89 return { version: 1, bias: record.bias, threshold: record.threshold, features };
90}
91
92/** Shannon entropy in bits per character. Kept local so this file imports nothing. */
93function entropyOf(value: string): number {
94 if (value.length === 0) return 0;
95 const counts = new Map<string, number>();
96 for (const ch of value) counts.set(ch, (counts.get(ch) ?? 0) + 1);
97 let bits = 0;
98 for (const count of counts.values()) {
99 const p = count / value.length;
100 bits -= p * Math.log2(p);
101 }
102 return bits;
103}
104
105/**
106 * The feature vector of one candidate value, in [0,1] per feature so a weight's
107 * size means the same thing across features.
108 */
109export function featuresOf(value: string): Record<FeatureName, number> {
110 const length = value.length;
111 let upper = 0;
112 let lower = 0;
113 let digit = 0;
114 let symbol = 0;
115 for (const ch of value) {
116 if (ch >= "A" && ch <= "Z") upper += 1;
117 else if (ch >= "a" && ch <= "z") lower += 1;
118 else if (ch >= "0" && ch <= "9") digit += 1;
119 else symbol += 1;
120 }
121 const ratio = (n: number) => (length === 0 ? 0 : n / length);
122 return {
123 // Length is capped at 64: past that the signal is already saturated.
124 length: Math.min(length, 64) / 64,
125 entropy: Math.min(entropyOf(value), 6) / 6,
126 upper: ratio(upper),
127 lower: ratio(lower),
128 digit: ratio(digit),
129 symbol: ratio(symbol),
130 hexish: /^[0-9a-fA-F]+$/.test(value) && length >= 16 ? 1 : 0,
131 base64ish: /^[A-Za-z0-9+/=_-]+$/.test(value) && length >= 16 ? 1 : 0,
132 camel: upper > 0 && lower > 0 ? 1 : 0,
133 separator: /[_-]/.test(value) ? 1 : 0,
134 // A vendor prefix (`sk_`, `ghp_`, `xoxb-`, `AIza`) sits in the first few
135 // characters; its presence is the strongest single predictor there is.
136 prefixed: /^[A-Za-z]{2,6}[_-]/.test(value) ? 1 : 0,
137 wordlike: /^[A-Za-z]+$/.test(value) ? 1 : 0,
138 };
139}
140
141/**
142 * Probability that a candidate value is a secret, in [0,1].
143 *
144 * Pure arithmetic over the weights file: no I/O, no clock, no randomness, so the
145 * same value always scores the same and a placeholder stays stable.
146 */
147export function scoreValue(model: ClassifierModel, value: string): number {
148 const features = featuresOf(value);
149 let logit = model.bias;
150 for (const [name, weight] of Object.entries(model.features)) {
151 if (!(FEATURE_NAMES as readonly string[]).includes(name)) continue;
152 logit += weight * features[name as FeatureName];
153 }
154 if (logit > CLAMP) return 1;
155 if (logit < -CLAMP) return 0;
156 return 1 / (1 + Math.exp(-logit));
157}
158
159/** Whether the model calls this value a secret, at its own threshold. */
160export function isSecret(model: ClassifierModel, value: string): boolean {
161 return scoreValue(model, value) >= model.threshold;
162}
163hooks/rules.ts 75 lines1// The rule corpus, as a module.
2//
3// `rules.generated.ts` is generated data: 220 rules with per-rule provenance,
4// written by `tools/gen-rules.py` from gitleaks (MIT) and never edited by hand.
5// This file is the API the rest of the mod imports, so the engine has one place
6// to read "what rules exist" without depending on the generator's output layout.
7//
8// Keeping the generated data and the accessor apart matters for review: the gate
9// (`tools/check-rules.mjs`) checks the data file directly, so a change to this
10// accessor cannot hide a rule that lost its provenance.
11
12import { RULES as GENERATED_RULES, type Rule } from "./rules.generated.ts";
13
14export {
15 CORPUS_SHA256,
16 GLOBAL_ALLOW_REGEXES,
17 GLOBAL_STOPWORDS,
18 type Rule,
19} from "./rules.generated.ts";
20
21export { ALLOWLIST, isAllowed, type AllowEntry } from "./allowlist.ts";
22
23/**
24 * The id a person writes after `/redact rule off` to silence the field-name
25 * password detector.
26 *
27 * The detector is engine-owned rather than gitleaks-derived: its rule is the
28 * field name, and it deliberately runs without an entropy floor, which the
29 * generated corpus cannot express. Listing it here puts it on the same L3 id
30 * surface as every shipped rule.
31 */
32export const PASSWORD_VALUE_RULE_ID = "password-value";
33
34const PASSWORD_VALUE_DESCRIPTOR: Rule = {
35 id: PASSWORD_VALUE_RULE_ID,
36 label: "PASSWORD_VALUE",
37 // Never compiled into the corpus scan (scrub.ts filters ENGINE_RULE_IDS); the
38 // id exists so L3 can validate it. A never-matching source keeps an accidental
39 // compile inert rather than redacting the word "password".
40 regex: "(?!)",
41 flags: "",
42 groups: 0,
43 secretGroup: null,
44 entropy: null,
45 keywords: [],
46 allow: [],
47 kind: "generic",
48 source: "https://github.com/ezoushen/claude-mods/blob/main/prompt-redact/hooks/scrub.ts#password-value",
49};
50
51/**
52 * Every id L3 may switch. The generated corpus first, then engine-owned
53 * detectors that are not part of it. `scrub.ts` excludes the latter from the
54 * corpus scan and runs each in its own pass.
55 */
56export const RULES: readonly Rule[] = [...GENERATED_RULES, PASSWORD_VALUE_DESCRIPTOR];
57
58/** Rules grouped by the family that makes them cheap to run. */
59export function rulesByKind(): { prefix: Rule[]; vendorKeyword: Rule[]; generic: Rule[] } {
60 const prefix: Rule[] = [];
61 const vendorKeyword: Rule[] = [];
62 const generic: Rule[] = [];
63 for (const rule of GENERATED_RULES) {
64 if (rule.kind === "prefix") prefix.push(rule);
65 else if (rule.kind === "generic") generic.push(rule);
66 else vendorKeyword.push(rule);
67 }
68 return { prefix, vendorKeyword, generic };
69}
70
71/** The generated corpus size, for log lines and `/redact why`. */
72export function ruleCount(): number {
73 return GENERATED_RULES.length;
74}
75hooks/allowlist.ts 83 lines1// The allowlist is the reason the generic detector can run at a low threshold.
2//
3// A redactor's false positive is cheap: Claude reads `[REDACTED:...]` where a
4// meaningless hash used to be. But there is one false positive it cannot afford
5// -- redacting a value the *task needs*. A git SHA, an object id, a lockfile
6// integrity string, a resource ARN: take those away and the model can no longer
7// do the thing you asked. So the rule is:
8//
9// be maximally aggressive on secrets, and name every benign high-entropy
10// shape you must leave alone.
11//
12// Without this list the generic rule has to be tuned so conservatively that it
13// misses real credentials. With it, the threshold drops and recall rises.
14//
15// Each entry matches a *whole candidate value* (anchored), never a substring, so
16// an allowlist hit can never carve a hole in the middle of a real secret.
17
18export type AllowEntry = {
19 /** Why this shape is benign; quoted in `RULES.md` and `/redact why`. */
20 readonly why: string;
21 readonly pattern: RegExp;
22};
23
24const A = (why: string, source: string, flags = ""): AllowEntry => ({
25 why,
26 pattern: new RegExp(`^(?:${source})$`, flags),
27});
28
29export const ALLOWLIST: readonly AllowEntry[] = [
30 // --- content hashes: the classic high-entropy non-secret -------------------
31 A("git commit SHA-1 (content address, not a credential)", "[0-9a-f]{40}", "i"),
32 A("git blob/tree SHA-1 with prefix", "(?:blob|tree|commit)\\s[0-9a-f]{40}", "i"),
33 A("SHA-256 digest (content address, not a credential)", "[0-9a-f]{64}", "i"),
34 A("`sha256:` image digest", "sha256:[0-9a-f]{64}", "i"),
35 A("MD5 / 32-hex checksum", "[0-9a-f]{32}", "i"),
36
37 // --- identifiers: needed to reference objects in code ---------------------
38 A("UUID / database id / k8s resource UID", "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "i"),
39 A("MongoDB ObjectId", "[0-9a-f]{24}", "i"),
40 A("ULID -- a time-sortable identifier, not a credential", "[0-9A-HJKMNP-TV-Z]{26}", "i"),
41 A("AWS ARN -- a resource identifier; nothing to revoke", "arn:aws[a-z-]*:[a-z0-9-]+:[a-z0-9-]*:[0-9]{0,12}:[-\\w./:*]+", "i"),
42 A("AWS account id (12 digits)", "[0-9]{12}"),
43 A("IPv4 address -- a host, not a credential", "(?:\\d{1,3}\\.){3}\\d{1,3}"),
44 A("email address (PII is out of scope; names are not rule-detectable)", "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"),
45 A("semantic version", "v?\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?"),
46 A("ISO-8601 timestamp", "\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?"),
47 A("hex colour value from a stylesheet", "#[0-9a-f]{3,8}", "i"),
48 A("localhost URL", "https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?(?:/\\S*)?", "i"),
49
50 // --- integrity / provenance metadata --------------------------------------
51 A("subresource integrity (SRI)", "sha(?:256|384|512)-[A-Za-z0-9+/]{27,}={0,2}"),
52 A("npm / lockfile integrity -- must carry the sha<N>- prefix; a bare base64 run is a credential, not a digest", "sha(?:1|256|384|512)-[A-Za-z0-9+/]{27,}={0,2}"),
53 A("base64 data URI (inline asset)", "data:[a-z-]+/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+", "i"),
54 A("bcrypt hash -- already hashed, not a usable credential", "\\$2[abxy]\\$\\d{2}\\$[./A-Za-z0-9]{53}"),
55 A("SSH *public* key (public half; nothing to revoke)", "(?:ssh-(?:rsa|ed25519|dss)|ecdsa-sha2-\\S+)\\s+AAAA[A-Za-z0-9+/]{20,}={0,3}(?:\\s+\\S+)?"),
56 A("PUBLIC certificate block", "-----BEGIN CERTIFICATE-----[\\s\\S]*?-----END CERTIFICATE-----"),
57
58 // --- the placeholder lexicon: documentation and fixtures -------------------
59 // These are the words that mark a value as an example. They are the single
60 // most valuable allowlist family, and the reason not to mask the value before
61 // judging it: masking would delete the word "EXAMPLE", which is the evidence.
62 A("documentation placeholder", "(?:changeme|change_me|change-me|replace[-_ ]?me|your[-_ ]?key|your[-_ ]?token|your[-_ ]?secret|placeholder|redacted|dummy|fake|sample|example|test[-_ ]?value)", "i"),
63 A("schema keyword that fills a password field, not a password", "required", "i"),
64 A("template variable", "\\$\\{[^}]{0,200}\\}|\\{\\{[^}]{0,200}\\}\\}|<[A-Za-z_][A-Za-z0-9_ .:-]{0,60}>|\\$\\w+|%[A-Z_]+%"),
65 A("explicit redaction marker", "[xX]{3,}|\\*{3,}|\\.{3,}|<redacted>|\\[REDACTED[^\\]]*\\]"),
66
67 // --- the vendor sample credentials that are *known* public ----------------
68 A("AWS documented example key pair (public sample, never usable)", "REDACTED-AWS-ACCESS-KEY-BY-SLOPSHOPPER|wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", ""),
69];
70
71/** True when `value` is a benign shape that must reach the model unchanged. */
72export function isAllowed(value: string, extra: readonly RegExp[] = []): string | null {
73 const v = value.trim();
74 if (!v) return "empty";
75 for (const entry of ALLOWLIST) {
76 if (entry.pattern.test(v)) return entry.why;
77 }
78 for (const re of extra) {
79 if (re.test(v)) return "user allowlist";
80 }
81 return null;
82}
83hooks/structural.ts 551 lines1// Structural detectors: shapes that carry their own proof.
2//
3// A regex says "this looks like a key". A checksum says "this *is* one". Luhn,
4// IBAN mod-97, a JWT whose header decodes to JSON with an `alg`, a PEM block with
5// a matching END line -- these carry a self-check, so they run at effectively
6// 100% precision and need no entropy gate, no keyword anchor and no context.
7// The URL and Basic-auth detectors below prove themselves the same way: a URL
8// userinfo *is* the credential slot the RFC defines, and a Basic-auth value
9// decodes to `user:pass` or it is not a Basic-auth credential.
10//
11// They are also the one family that must NOT be a regex. `private-key` upstream
12// ends in `{64,}`: capping it would redact a key's head and leave its tail in the
13// transcript. So the PEM scanner walks the block linearly and redacts the whole
14// span. Same for a JWT: decode the header rather than pattern-match base64url.
15//
16// Everything here is linear in the row length -- no backtracking, no `.*`.
17
18import { isAllowed } from "./allowlist.ts";
19
20export type Hit = {
21 /** Placeholder label: [REDACTED:<label>#<hash>]. */
22 readonly label: string;
23 readonly start: number;
24 readonly end: number;
25 readonly value: string;
26 /** Which detector produced it, for `/redact why`. */
27 readonly by: string;
28};
29
30const PEM_BEGIN = /-----BEGIN ([A-Z0-9 ]*)PRIVATE KEY-----/g;
31const PEM_END_ANY = /-----END ([A-Z0-9 ]*)PRIVATE KEY-----/g;
32
33/**
34 * PEM private-key blocks: BEGIN..END, entirely redacted.
35 *
36 * Linear scan, no regex over the body. An unterminated block redacts from BEGIN
37 * to end-of-row (fail closed: a truncated key still must not survive).
38 */
39export function pemPrivateKeys(text: string): Hit[] {
40 const hits: Hit[] = [];
41 let cursor = 0;
42 for (;;) {
43 PEM_BEGIN.lastIndex = cursor;
44 const begin = PEM_BEGIN.exec(text);
45 if (!begin) break;
46 const label = begin[1] ?? "";
47 const from = begin.index;
48 PEM_END_ANY.lastIndex = begin.index + begin[0].length;
49 let end = text.length; // beyond the last body character (unterminated case)
50 let endMarkerAt = -1; // where the matching END marker text starts
51 for (;;) {
52 const candidate = PEM_END_ANY.exec(text);
53 if (!candidate) break;
54 if ((candidate[1] ?? "") === label) {
55 endMarkerAt = candidate.index;
56 end = endMarkerAt + candidate[0].length;
57 break;
58 }
59 }
60 // Fail closed on an unterminated block: the row itself may have been cut
61 // mid-paste, so a truncated key still must not survive (BEGIN..EOL).
62 const body = text.slice(begin.index + begin[0].length, endMarkerAt < 0 ? end : endMarkerAt);
63 if (endMarkerAt >= 0 && !derHeaderMatchesPayload(body)) {
64 cursor = end;
65 continue;
66 }
67 hits.push({
68 label: label.trim() ? `${label.trim().replace(/\s+/g, "_")}_PRIVATE_KEY` : "PRIVATE_KEY",
69 start: from,
70 end,
71 value: text.slice(from, end),
72 by: "pem",
73 });
74 cursor = end;
75 }
76 return hits;
77}
78
79/**
80 * The DER length header against the payload it precedes (X.690 / RFC 7468).
81 *
82 * A PEM key body is base64 DER whose outer element is a SEQUENCE (0x30) and
83 * whose length octets must be well formed and must not be contradicted by
84 * the payload. Two directions, not one:
85 *
86 * - The payload running PAST the declared structure end (content declares
87 * fewer bytes than follow the header) is a contradiction: the block is
88 * not the DER it claims to be, so it is not treated as a key.
89 * - The payload falling SHORT of the declared structure end is a truncated
90 * paste of a real key. The row may have been cut mid-paste, so this side
91 * fails closed and the block stays redacted (DECISIONS #3: truncation
92 * must not survive).
93 *
94 * The length octets themselves must be a canonical DER encoding: 0x80
95 * (indefinite) and long forms over four octets are rejected, and a long
96 * form under 0x80 bytes is non-minimal, so rejected.
97 */
98function derHeaderMatchesPayload(body: string): boolean {
99 const bytes = pemBodyBytes(body);
100 if (!bytes || bytes.length < 4) return false;
101 if (bytes[0] !== 0x30) return false;
102 const first = bytes[1];
103 let headerOctets: number;
104 let claimed: number;
105 if (first < 0x80) {
106 headerOctets = 2;
107 claimed = first;
108 } else if (first === 0x80 || first > 0x84 || bytes.length < 2 + (first - 0x80)) {
109 return false; // indefinite, non-representable, or truncated header
110 } else {
111 headerOctets = 2 + (first - 0x80);
112 claimed = 0;
113 for (const octet of bytes.slice(2, headerOctets)) claimed = (claimed << 8) | octet;
114 if (claimed < 0x80) return false; // non-minimal long form
115 }
116 const available = bytes.length - headerOctets;
117 if (claimed === 0) return false; // a SEQUENCE with no content is no key
118 return claimed >= available;
119}
120
121/**
122 * Decode a PEM body to bytes, whitespace ignored. A `=`-padded tail is
123 * trimmed -- only whole bytes count, and the leftover shift bits of the
124 * final sextet are discarded. Returns null for a body carrying anything
125 * outside the base64 alphabet.
126 */
127function pemBodyBytes(body: string): number[] | null {
128 const out: number[] = [];
129 let buffer = 0;
130 let bits = 0;
131 for (const ch of body.replace(/\s+/g, "").replace(/=+$/, "")) {
132 const value = B64_ALPHABET.indexOf(ch);
133 if (value < 0) return null;
134 buffer = (buffer << 6) | value;
135 bits += 6;
136 if (bits >= 8) {
137 bits -= 8;
138 out.push((buffer >> bits) & 0xff);
139 buffer &= (1 << bits) - 1;
140 }
141 }
142 return out;
143}
144
145const B64URL = /[A-Za-z0-9_-]{8,}/g;
146
147const B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
148
149/**
150 * Decode base64url without `atob`: the mod sandbox is not guaranteed to expose
151 * web globals, and a silently missing decoder would mean silent JWT blindness --
152 * a false negative in a redactor, the one kind it cannot afford.
153 */
154function decodeSegment(seg: string): string | null {
155 const clean = seg.replace(/-/g, "+").replace(/_/g, "/");
156 let out = "";
157 let buffer = 0;
158 let bits = 0;
159 for (const ch of clean) {
160 const value = B64_ALPHABET.indexOf(ch);
161 if (value < 0) return null;
162 buffer = (buffer << 6) | value;
163 bits += 6;
164 if (bits >= 8) {
165 bits -= 8;
166 out += String.fromCharCode((buffer >> bits) & 0xff);
167 }
168 }
169 return out;
170}
171
172/**
173 * JSON Web Tokens, proven structurally: the first segment must base64url-decode
174 * to a JSON object with an `alg` field. That check is what separates a real JWT
175 * from any three dotted base64url strings, and it costs no entropy tuning.
176 */
177export function jwts(text: string): Hit[] {
178 const hits: Hit[] = [];
179 const seen = new Set<number>();
180 B64URL.lastIndex = 0;
181 for (;;) {
182 const first = B64URL.exec(text);
183 if (!first) break;
184 const start = first.index;
185 if (seen.has(start)) continue;
186 // The token runs to the end of its dotted run.
187 const rest = /^[A-Za-z0-9_-]*\.([A-Za-z0-9_-]{8,})\.([A-Za-z0-9_-]*)/.exec(text.slice(start + first[0].length));
188 if (!rest) continue;
189 const payload = rest[1] ?? "";
190 const headerText = decodeSegment(first[0]);
191 if (!headerText) continue;
192 let header: unknown;
193 try {
194 header = JSON.parse(headerText);
195 } catch {
196 continue;
197 }
198 if (typeof header !== "object" || header === null) continue;
199 const alg = (header as Record<string, unknown>).alg;
200 if (typeof alg !== "string" || !alg) continue;
201 const end = start + first[0].length + 1 + payload.length + 1 + (rest[2] ?? "").length;
202 seen.add(start);
203 hits.push({ label: "JWT", start, end, value: text.slice(start, end), by: "jwt" });
204 }
205 return hits;
206}
207
208/** Luhn checksum: the mod-10 test every payment card number satisfies. */
209export function luhnOk(digits: string): boolean {
210 let sum = 0;
211 let double = false;
212 for (let i = digits.length - 1; i >= 0; i--) {
213 const code = digits.charCodeAt(i) - 48;
214 if (code < 0 || code > 9) return false;
215 let d = code;
216 if (double) {
217 d *= 2;
218 if (d > 9) d -= 9;
219 }
220 sum += d;
221 double = !double;
222 }
223 return sum % 10 === 0;
224}
225
226const CARDISH = /(?<![0-9])(?:\d[ -]?){12,18}\d(?![0-9])/g;
227
228/** Payment card numbers: 13-19 digits whose Luhn checksum verifies. */
229export function cards(text: string): Hit[] {
230 const hits: Hit[] = [];
231 for (const m of text.matchAll(CARDISH)) {
232 const raw = m[0];
233 const digits = raw.replace(/[^0-9]/g, "");
234 if (digits.length < 13 || digits.length > 19) continue;
235 if (!luhnOk(digits)) continue;
236 const start = m.index ?? 0;
237 hits.push({ label: "CARD_NUMBER", start, end: start + raw.length, value: raw, by: "luhn" });
238 }
239 return hits;
240}
241
242/** IBAN mod-97, the checksum defined by ISO 13616. */
243export function ibanOk(candidate: string): boolean {
244 const iban = candidate.replace(/\s+/g, "").toUpperCase();
245 if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$/.test(iban)) return false;
246 const rearranged = iban.slice(4) + iban.slice(0, 4);
247 let remainder = 0;
248 for (const ch of rearranged) {
249 const code = ch.charCodeAt(0);
250 const chunk = code >= 48 && code <= 57 ? String(code - 48) : String(code - 55);
251 for (const digit of chunk) remainder = (remainder * 10 + Number(digit)) % 97;
252 }
253 return remainder === 1;
254}
255
256const IBANISH = /(?<![A-Za-z0-9])[A-Z]{2}[0-9]{2}[A-Z0-9 ]{10,30}(?![A-Za-z0-9])/g;
257
258/** IBANs whose mod-97 checksum verifies. */
259export function ibans(text: string): Hit[] {
260 const hits: Hit[] = [];
261 for (const m of text.matchAll(IBANISH)) {
262 if (!ibanOk(m[0])) continue;
263 const start = m.index ?? 0;
264 hits.push({ label: "IBAN", start, end: start + m[0].length, value: m[0], by: "iban" });
265 }
266 return hits;
267}
268
269/**
270 * US Social Security numbers, under the SSA's own issuance rules (FICA):
271 * area 001-899 except 666, group 01-99, serial 0001-9999. A number that also
272 * carries a Verhoeff check digit is accepted as well, but the digit is not a
273 * precondition.
274 *
275 * The issuance exclusions are the discriminator, and they are what keeps this
276 * from matching every 9-digit run. Verhoeff is *not* part of the real SSN
277 * scheme: it was proposed for it and never adopted, so a genuinely issued
278 * number carries no check digit. Requiring one as a conjunct dropped roughly
279 * nine of every ten real SSNs from the redactor, which is the wrong direction
280 * for a redactor's guarantee. Hence the union: issuance rules OR a valid
281 * check digit.
282 */
283export function ssns(text: string): Hit[] {
284 const hits: Hit[] = [];
285 for (const m of text.matchAll(/(?<![0-9])(\d{3})-(\d{2})-(\d{4})(?![0-9])/g)) {
286 const area = Number(m[1]);
287 const group = Number(m[2]);
288 const serial = Number(m[3]);
289 const issuanceOk = area !== 0 && area !== 666 && area <= 899 && group !== 0 && serial !== 0;
290 if (!issuanceOk && !verhoeffOk(m[1] + m[2] + m[3])) continue;
291 const start = m.index ?? 0;
292 hits.push({ label: "SSN", start, end: start + m[0].length, value: m[0], by: "fica" });
293 }
294 return hits;
295}
296
297// --- Verhoeff check digit ----------------------------------------------------
298
299/**
300 * The Verhoeff check digit (Verhoeff 1969): the decimal checksum over the
301 * dihedral group D5 that detects every single-digit error and every adjacent
302 * transposition. Tables are the ones Verhoeff published; worked examples
303 * (check digit 3 for 236, validating 2363; check digit 7 for 942; 12345 -> 1
304 * with 123451 valid and its twins 122451/128451 invalid) settle the reading
305 * of the tables in tests.
306 */
307const VERHOEFF_D = [
308 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
309 [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
310 [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
311 [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
312 [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
313 [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
314 [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
315 [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
316 [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
317 [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
318];
319
320const VERHOEFF_P = [
321 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
322 [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
323 [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
324 [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
325 [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
326 [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
327 [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
328 [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
329];
330
331const VERHOEFF_INV = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
332
333/** D5 checksum of a digit string, digits taken right-to-left. */
334function verhoeffSum(digits: string, positionShift: number): number {
335 let c = 0;
336 for (let i = digits.length - 1; i >= 0; i--) {
337 c = VERHOEFF_D[c][VERHOEFF_P[((digits.length - 1 - i) + positionShift) % 8][Number(digits[i])]];
338 }
339 return c;
340}
341
342/**
343 * The check digit to append to `body` so the concatenation validates.
344 */
345export function verhoeffCheckDigit(body: string): number {
346 return VERHOEFF_INV[verhoeffSum(body, 1)];
347}
348
349/**
350 * True when `digits` carries a correct Verhoeff check digit as its last
351 * digit -- equivalently, `digits` is `prefix + verhoeffCheckDigit(prefix)`.
352 */
353export function verhoeffOk(digits: string): boolean {
354 return verhoeffSum(digits, 0) === 0;
355}
356
357
358const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
359// A base32 run is uppercase only: `0`, `1`, `8`, `9` are not in the alphabet, and
360// lowercase means base64 or an identifier. RFC 4648 pads to a multiple of eight.
361const BASE32_RUN = /(?<![A-Za-z0-9])[A-Z2-7]{16,64}={0,6}(?![A-Za-z0-9])/g;
362// TOTP secrets have no vendor prefix, so the anchor has to be the surrounding
363// words. This is the one structural family that needs context, and the reason it
364// is checked last: without one of these words nearby, 16 uppercase characters are
365// far more likely to be an identifier than an HMAC key.
366const TOTP_CONTEXT =
367 /(?:totp|otp|2fa|mfa|two[-_ ]?factor|authenticator|one[-_ ]?time|gauth|otpauth|backup[-_ ]?code|otp_secret|seed)/i;
368
369/**
370 * Bytes encoded by a base32 body, or null when the body is not base32.
371 *
372 * The byte count is what distinguishes a real HMAC key from a coincidence: RFC
373 * 6238 secrets are 10, 20, 32 or 64 bytes in practice, so a 16-character run
374 * (10 bytes) verifies and a 34-character word does not land on a key length.
375 */
376function base32Bytes(body: string): number | null {
377 let buffer = 0;
378 let bits = 0;
379 let bytes = 0;
380 for (const ch of body) {
381 const value = BASE32_ALPHABET.indexOf(ch);
382 if (value < 0) return null;
383 buffer = (buffer << 5) | value;
384 bits += 5;
385 while (bits >= 8) {
386 bits -= 8;
387 bytes += 1;
388 buffer &= (1 << bits) - 1;
389 }
390 }
391 return bytes;
392}
393
394/**
395 * base32 TOTP secrets, proven structurally: a base32 body whose decoded length is
396 * a plausible HMAC key size, next to a word that says it is a one-time-password
397 * seed. Neither half alone is enough, which is what keeps the precision high.
398 */
399export function totpSecrets(text: string): Hit[] {
400 const hits: Hit[] = [];
401 for (const m of text.matchAll(BASE32_RUN)) {
402 const raw = m[0];
403 const body = raw.replace(/=+$/, "");
404 const bytes = base32Bytes(body);
405 if (bytes === null) continue;
406 if (bytes < 10 || bytes > 64) continue;
407 const start = m.index ?? 0;
408 const before = text.slice(Math.max(0, start - 48), start);
409 const after = text.slice(start + raw.length, Math.min(text.length, start + raw.length + 24));
410 if (!TOTP_CONTEXT.test(before) && !TOTP_CONTEXT.test(after)) continue;
411 hits.push({ label: "TOTP_SECRET", start, end: start + raw.length, value: raw, by: "base32" });
412 }
413 return hits;
414}
415
416// --- URL userinfo -----------------------------------------------------------
417
418/**
419 * The userinfo slot of an HTTP(S) family URL: `scheme://user:password@host`.
420 *
421 * The password, and only the password, is the credential. Scheme, user, host,
422 * port and path are the structure a person pastes a config to keep working, so
423 * the span ends exactly at the character before `@` and begins exactly at the
424 * character after the user's `:`. An empty password (`scheme://user:@host`) is
425 * no credential and never matches.
426 *
427 * Every quantifier is bounded and the scan is linear in the row length: the
428 * class runs stop at `:`, `@`, `/` and whitespace, which is where the userinfo
429 * definition ends anyway.
430 */
431const URL_USERINFO =
432 /(?<![-_.A-Za-z0-9])([A-Za-z][A-Za-z0-9+.-]{1,20}:\/\/[A-Za-z0-9._~%!$&'()*+,;={}-]{1,128}:)([^:/@\s"'`\\]{1,2048})@/g;
433
434/**
435 * Passwords inside URL userinfo, with rule id `url-userinfo`.
436 *
437 * The placeholder lexicon is honoured inside the detector: the allowlist's
438 * whole-value contract means a templated or documented password
439 * (`${DATABASE_PASSWORD}`, `changeme`, `<password>`) is not redactable in the
440 * first place, so no candidate is emitted for one.
441 */
442export function urlPasswords(text: string): Hit[] {
443 const hits: Hit[] = [];
444 for (const m of text.matchAll(URL_USERINFO)) {
445 const password = m[2];
446 if (!password) continue;
447 if (isAllowed(password) !== null) continue;
448 const start = (m.index ?? 0) + (m[1] ?? "").length;
449 hits.push({
450 label: "URL_PASSWORD",
451 start,
452 end: start + password.length,
453 value: password,
454 by: "url-userinfo",
455 });
456 }
457 return hits;
458}
459
460// --- Basic-auth headers -----------------------------------------------------
461
462/**
463 * A base64 candidate: the standard alphabet with optional padding, delimited by
464 * anything that is not itself a base64 or base64url character. The floor is six
465 * alphabet characters -- the smallest *padded* form (`dXNlcg==`), so the
466 * lower-boundary case reaches the decode proof instead of bouncing off a length
467 * gate no test can see. Bounded, and the `.` in the boundary classes keeps a
468 * JWT's dotted segments out of a single run.
469 */
470const B64_TOKEN = /(?<![A-Za-z0-9+/=.-])([A-Za-z0-9+/]{6,4096}={0,2})(?![A-Za-z0-9+/=.-])/g;
471
472/**
473 * The word `Basic` immediately before a candidate: the one header this
474 * detector answers to, and the anchor that separates an HTTP Basic credential
475 * from any base64 run. Variants beyond `Basic` are out of scope by design.
476 */
477const BASIC_SCHEME = /\bbasic[ \t]{1,8}$/i;
478
479/**
480 * Decode standard base64 (with optional padding) without `atob`, for the same
481 * reason the JWT header decodes here instead of trusting a shape: a missing
482 * decoder would mean silent blindness. Rejects anything whose bytes are not
483 * printable ASCII -- that requirement is most of the precision, because it
484 * refuses the prose words and identifiers a b64-charset regex happily matches.
485 */
486function decodeB64(seg: string): string | null {
487 // A run whose length is 1 mod 4 cannot be base64, padded or not.
488 if (seg.length % 4 === 1 && !seg.endsWith("=")) return null;
489 const body = seg.replace(/=+$/, "");
490 let out = "";
491 let buffer = 0;
492 let bits = 0;
493 for (const ch of body) {
494 const value = B64_ALPHABET.indexOf(ch);
495 if (value < 0) return null;
496 buffer = (buffer << 6) | value;
497 bits += 6;
498 if (bits >= 8) {
499 bits -= 8;
500 const byte = (buffer >> bits) & 0xff;
501 if (byte < 0x20 || byte > 0x7e) return null;
502 out += String.fromCharCode(byte);
503 }
504 }
505 return out;
506}
507
508/**
509 * `Authorization: Basic` credentials, with rule id `basic-auth`.
510 *
511 * The span is the base64 value only, so the header keeps its scheme word.
512 * The proof is the same shape the URL detector leans on, decoded: the value
513 * must decode (padding allowed) to printable ASCII carrying a `user:pass`
514 * split with both halves non-empty. A base64 value that does not decode to
515 * that shape is left untouched -- `Basic dXNlcg==` and `Basic not-base64!`
516 * both survive verbatim.
517 */
518export function basicAuthCredentials(text: string): Hit[] {
519 const hits: Hit[] = [];
520 for (const m of text.matchAll(B64_TOKEN)) {
521 const token = m[1];
522 if (!token) continue;
523 const start = m.index ?? 0;
524 const before = text.slice(Math.max(0, start - 64), start);
525 if (!BASIC_SCHEME.test(before)) continue;
526 const decoded = decodeB64(token);
527 if (decoded === null) continue;
528 const colon = decoded.indexOf(":");
529 if (colon < 1) continue;
530 if (decoded.length - colon < 2) continue;
531 hits.push({ label: "BASIC_AUTH", start, end: start + token.length, value: token, by: "basic-auth" });
532 }
533 return hits;
534}
535
536/**
537 * Every structural detector, in the order their spans should win conflicts.
538 */
539export function structuralHits(text: string): Hit[] {
540 return [
541 ...pemPrivateKeys(text),
542 ...jwts(text),
543 ...ibans(text),
544 ...cards(text),
545 ...ssns(text),
546 ...totpSecrets(text),
547 ...urlPasswords(text),
548 ...basicAuthCredentials(text),
549 ];
550}
551hooks/rules.generated.ts 265 lines1// GENERATED FILE -- do not edit by hand.
2// Source: tools/gen-rules.py over prompt-redact/vendor/gitleaks.toml
3// gitleaks.toml sha256: e163e53b9e7e8a8511e77271e2b323ed057759542a6d988258afe3a1fa329caf
4// 220 rules: 95 prefix-anchored, 123 vendor-keyword, 2 generic.
5
6export type Rule = {
7 /** gitleaks rule id, the provenance key. */
8 readonly id: string;
9 /** Label a placeholder carries: [REDACTED:<label>#<hash>]. */
10 readonly label: string;
11 /** JavaScript RegExp source, converted from the upstream RE2 pattern. */
12 readonly regex: string;
13 readonly flags: string;
14 /** Capture groups present; the runtime redacts the highest-entropy one. */
15 readonly groups: number;
16 /** Which group holds the value, when gitleaks states it explicitly. */
17 readonly secretGroup: number | null;
18 /** Minimum Shannon entropy of the value, when the rule gates on it. */
19 readonly entropy: number | null;
20 /** Prefilter: the rule is only run when one of these appears in the row. */
21 readonly keywords: readonly string[];
22 /** Per-rule allowlist: a value matching one of these is left alone. */
23 readonly allow: readonly string[];
24 readonly kind: 'prefix' | 'vendor-keyword' | 'generic';
25 /** Provenance, one URL per rule. */
26 readonly source: string;
27};
28
29export const RULES: readonly Rule[] = [
30 { id: "1password-secret-key", label: "1PASSWORD_SECRET_KEY", regex: "\\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 3.8, keywords: ["a3-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#1password-secret-key" },
31 { id: "1password-service-account-token", label: "1PASSWORD_SERVICE_ACCOUNT_TOKEN", regex: "ops_eyJ[a-zA-Z0-9+/]{250,4096}={0,3}", flags: "i", groups: 0, secretGroup: null, entropy: 4.0, keywords: ["ops_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#1password-service-account-token" },
32 { id: "adafruit-api-key", label: "ADAFRUIT_API_KEY", regex: "[\\w.-]{0,50}?(?:adafruit)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9_-]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["adafruit"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#adafruit-api-key" },
33 { id: "adobe-client-id", label: "ADOBE_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:adobe)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["adobe"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#adobe-client-id" },
34 { id: "adobe-client-secret", label: "ADOBE_CLIENT_SECRET", regex: "\\b(p8e-[a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["p8e-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#adobe-client-secret" },
35 { id: "age-secret-key", label: "AGE_SECRET_KEY", regex: "AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}", flags: "i", groups: 0, secretGroup: null, entropy: null, keywords: ["age-secret-key-1"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#age-secret-key" },
36 { id: "airtable-api-key", label: "AIRTABLE_API_KEY", regex: "[\\w.-]{0,50}?(?:airtable)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{17})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["airtable"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#airtable-api-key" },
37 { id: "airtable-personnal-access-token", label: "AIRTABLE_PERSONNAL_ACCESS_TOKEN", regex: "\\b(pat[A-Za-z0-9]{14}\\.[a-f0-9]{64})\\b", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["airtable"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#airtable-personnal-access-token" },
38 { id: "algolia-api-key", label: "ALGOLIA_API_KEY", regex: "[\\w.-]{0,50}?(?:algolia)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["algolia"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#algolia-api-key" },
39 { id: "alibaba-access-key-id", label: "ALIBABA_ACCESS_KEY_ID", regex: "\\b(LTAI[a-z0-9]{20})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["ltai"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#alibaba-access-key-id" },
40 { id: "alibaba-secret-key", label: "ALIBABA_SECRET_KEY", regex: "[\\w.-]{0,50}?(?:alibaba)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{30})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["alibaba"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#alibaba-secret-key" },
41 { id: "anthropic-admin-api-key", label: "ANTHROPIC_ADMIN_API_KEY", regex: "\\b(sk-ant-admin01-[a-zA-Z0-9_\\-]{93}AA)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["sk-ant-admin01"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#anthropic-admin-api-key" },
42 { id: "anthropic-api-key", label: "ANTHROPIC_API_KEY", regex: "\\b(sk-ant-api03-[a-zA-Z0-9_\\-]{93}AA)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["sk-ant-api03"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#anthropic-api-key" },
43 { id: "artifactory-api-key", label: "ARTIFACTORY_API_KEY", regex: "\\bAKCp[A-Za-z0-9]{69}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 4.5, keywords: ["akcp"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#artifactory-api-key" },
44 { id: "artifactory-reference-token", label: "ARTIFACTORY_REFERENCE_TOKEN", regex: "\\bcmVmd[A-Za-z0-9]{59}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 4.5, keywords: ["cmvmd"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#artifactory-reference-token" },
45 { id: "asana-client-id", label: "ASANA_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:asana)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["asana"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#asana-client-id" },
46 { id: "asana-client-secret", label: "ASANA_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:asana)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["asana"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#asana-client-secret" },
47 { id: "atlassian-api-token", label: "ATLASSIAN_API_TOKEN", regex: "[\\w.-]{0,50}?(?:(?:ATLASSIAN|[Aa]tlassian)|(?:CONFLUENCE|[Cc]onfluence)|(?:JIRA|[Jj]ira))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\\x60'\"\\s;]|\\\\[nr]|$)|\\b(ATATT3[A-Za-z0-9_\\-=]{186})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: 3.5, keywords: ["atlassian", "confluence", "jira", "atatt3"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#atlassian-api-token" },
48 { id: "authress-service-client-access-key", label: "AUTHRESS_SERVICE_CLIENT_ACCESS_KEY", regex: "\\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\\.[a-z0-9]{4,6}\\.(?:acc)[_-][a-z0-9-]{10,32}\\.[a-z0-9+/_=-]{30,120})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["sc_", "ext_", "scauth_", "authress_"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#authress-service-client-access-key" },
49 { id: "aws-access-token", label: "AWS_ACCESS_TOKEN", regex: "\\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\\b", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["a3t", "akia", "asia", "abia", "acca"], allow: [".+EXAMPLE$"], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#aws-access-token" },
50 { id: "aws-amazon-bedrock-api-key-long-lived", label: "AWS_AMAZON_BEDROCK_API_KEY_LONG_LIVED", regex: "\\b(ABSK[A-Za-z0-9+/]{109,269}={0,2})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["absk"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#aws-amazon-bedrock-api-key-long-lived" },
51 { id: "aws-amazon-bedrock-api-key-short-lived", label: "AWS_AMAZON_BEDROCK_API_KEY_SHORT_LIVED", regex: "bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["bedrock-api-key-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#aws-amazon-bedrock-api-key-short-lived" },
52 { id: "azure-ad-client-secret", label: "AZURE_AD_CLIENT_SECRET", regex: "(?:^|[\\\\'\"\\x60\\s>=:(,)])([a-zA-Z0-9_~.]{3}\\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\\\'\"\\x60\\s<),])", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["q~"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#azure-ad-client-secret" },
53 { id: "beamer-api-token", label: "BEAMER_API_TOKEN", regex: "[\\w.-]{0,50}?(?:beamer)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(b_[a-z0-9=_\\-]{44})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["beamer"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#beamer-api-token" },
54 { id: "bitbucket-client-id", label: "BITBUCKET_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:bitbucket)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["bitbucket"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#bitbucket-client-id" },
55 { id: "bitbucket-client-secret", label: "BITBUCKET_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:bitbucket)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["bitbucket"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#bitbucket-client-secret" },
56 { id: "bittrex-access-key", label: "BITTREX_ACCESS_KEY", regex: "[\\w.-]{0,50}?(?:bittrex)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["bittrex"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#bittrex-access-key" },
57 { id: "bittrex-secret-key", label: "BITTREX_SECRET_KEY", regex: "[\\w.-]{0,50}?(?:bittrex)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["bittrex"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#bittrex-secret-key" },
58 { id: "cisco-meraki-api-key", label: "CISCO_MERAKI_API_KEY", regex: "[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Mm]eraki|MERAKI))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["meraki"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#cisco-meraki-api-key" },
59 { id: "clickhouse-cloud-api-secret-key", label: "CLICKHOUSE_CLOUD_API_SECRET_KEY", regex: "\\b(4b1d[A-Za-z0-9]{38})\\b", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["4b1d"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#clickhouse-cloud-api-secret-key" },
60 { id: "clojars-api-token", label: "CLOJARS_API_TOKEN", regex: "CLOJARS_[a-z0-9]{60}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["clojars_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#clojars-api-token" },
61 { id: "cloudflare-api-key", label: "CLOUDFLARE_API_KEY", regex: "[\\w.-]{0,50}?(?:cloudflare)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9_-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["cloudflare"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#cloudflare-api-key" },
62 { id: "cloudflare-global-api-key", label: "CLOUDFLARE_GLOBAL_API_KEY", regex: "[\\w.-]{0,50}?(?:cloudflare)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{37})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["cloudflare"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#cloudflare-global-api-key" },
63 { id: "cloudflare-origin-ca-key", label: "CLOUDFLARE_ORIGIN_CA_KEY", regex: "\\b(v1\\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["cloudflare", "v1.0-"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#cloudflare-origin-ca-key" },
64 { id: "codecov-access-token", label: "CODECOV_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:codecov)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["codecov"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#codecov-access-token" },
65 { id: "cohere-api-token", label: "COHERE_API_TOKEN", regex: "[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-zA-Z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["cohere", "co_api_key"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#cohere-api-token" },
66 { id: "coinbase-access-token", label: "COINBASE_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:coinbase)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9_-]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["coinbase"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#coinbase-access-token" },
67 { id: "confluent-access-token", label: "CONFLUENT_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:confluent)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["confluent"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#confluent-access-token" },
68 { id: "confluent-secret-key", label: "CONFLUENT_SECRET_KEY", regex: "[\\w.-]{0,50}?(?:confluent)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["confluent"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#confluent-secret-key" },
69 { id: "contentful-delivery-api-token", label: "CONTENTFUL_DELIVERY_API_TOKEN", regex: "[\\w.-]{0,50}?(?:contentful)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{43})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["contentful"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#contentful-delivery-api-token" },
70 { id: "curl-auth-header", label: "CURL_AUTH_HEADER", regex: "\\bcurl\\b(?:.{0,4096}?|.{0,4096}?(?:[\\r\\n]{1,2}.{0,4096}?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,4096}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,4096})|([\\w=~@.+/-]{8,4096}))|(?:(?:X-(?:[a-z]{1,4096}-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,4096}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,4096}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,4096})|([\\w=~@.+/-]{8,4096}))|(?:(?:X-(?:[a-z]{1,4096}-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,4096}))')(?:\\B|\\s|$)", flags: "i", groups: 8, secretGroup: null, entropy: 2.75, keywords: ["curl"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#curl-auth-header" },
71 { id: "curl-auth-user", label: "CURL_AUTH_USER", regex: "\\bcurl\\b(?:.{0,4096}|.{0,4096}(?:[\\r\\n]{1,2}.{0,4096}){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,4096}|[^:\"]{3,4096}:|[^:\"]{3,4096}:[^\"]{3,4096})\"|'([^:']{3,4096}:[^']{3,4096})'|((?:\"[^\"]{3,4096}\"|'[^']{3,4096}'|[\\w$@.-]{1,4096}):(?:\"[^\"]{3,4096}\"|'[^']{3,4096}'|[\\w${}@.-]{1,4096})))(?:\\s|$)", flags: "i", groups: 4, secretGroup: null, entropy: 2.0, keywords: ["curl"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#curl-auth-user" },
72 { id: "databricks-api-token", label: "DATABRICKS_API_TOKEN", regex: "\\b(dapi[a-f0-9]{32}(?:-\\d)?)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["dapi"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#databricks-api-token" },
73 { id: "datadog-access-token", label: "DATADOG_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:datadog)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["datadog"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#datadog-access-token" },
74 { id: "defined-networking-api-token", label: "DEFINED_NETWORKING_API_TOKEN", regex: "[\\w.-]{0,50}?(?:dnkey)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(dnkey-[a-z0-9=_\\-]{26}-[a-z0-9=_\\-]{52})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["dnkey"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#defined-networking-api-token" },
75 { id: "digitalocean-access-token", label: "DIGITALOCEAN_ACCESS_TOKEN", regex: "\\b(doo_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["doo_v1_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#digitalocean-access-token" },
76 { id: "digitalocean-pat", label: "DIGITALOCEAN_PAT", regex: "\\b(dop_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["dop_v1_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#digitalocean-pat" },
77 { id: "digitalocean-refresh-token", label: "DIGITALOCEAN_REFRESH_TOKEN", regex: "\\b(dor_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["dor_v1_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#digitalocean-refresh-token" },
78 { id: "discord-api-token", label: "DISCORD_API_TOKEN", regex: "[\\w.-]{0,50}?(?:discord)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["discord"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#discord-api-token" },
79 { id: "discord-client-id", label: "DISCORD_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:discord)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{18})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["discord"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#discord-client-id" },
80 { id: "discord-client-secret", label: "DISCORD_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:discord)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["discord"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#discord-client-secret" },
81 { id: "doppler-api-token", label: "DOPPLER_API_TOKEN", regex: "dp\\.pt\\.[a-z0-9]{43}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["dp.pt."], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#doppler-api-token" },
82 { id: "droneci-access-token", label: "DRONECI_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:droneci)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["droneci"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#droneci-access-token" },
83 { id: "dropbox-api-token", label: "DROPBOX_API_TOKEN", regex: "[\\w.-]{0,50}?(?:dropbox)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{15})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["dropbox"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#dropbox-api-token" },
84 { id: "dropbox-long-lived-api-token", label: "DROPBOX_LONG_LIVED_API_TOKEN", regex: "[\\w.-]{0,50}?(?:dropbox)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\\-_=]{43})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: null, keywords: ["dropbox"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#dropbox-long-lived-api-token" },
85 { id: "dropbox-short-lived-api-token", label: "DROPBOX_SHORT_LIVED_API_TOKEN", regex: "[\\w.-]{0,50}?(?:dropbox)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(sl\\.[a-z0-9\\-=_]{135})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["dropbox"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#dropbox-short-lived-api-token" },
86 { id: "duffel-api-token", label: "DUFFEL_API_TOKEN", regex: "duffel_(?:test|live)_[a-z0-9_\\-=]{43}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["duffel_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#duffel-api-token" },
87 { id: "dynatrace-api-token", label: "DYNATRACE_API_TOKEN", regex: "dt0c01\\.[a-z0-9]{24}\\.[a-z0-9]{64}", flags: "i", groups: 0, secretGroup: null, entropy: 4.0, keywords: ["dt0c01."], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#dynatrace-api-token" },
88 { id: "easypost-api-token", label: "EASYPOST_API_TOKEN", regex: "\\bEZAK[a-z0-9]{54}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["ezak"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#easypost-api-token" },
89 { id: "easypost-test-api-token", label: "EASYPOST_TEST_API_TOKEN", regex: "\\bEZTK[a-z0-9]{54}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["eztk"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#easypost-test-api-token" },
90 { id: "etsy-access-token", label: "ETSY_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:(?:ETSY|[Ee]tsy))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["etsy"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#etsy-access-token" },
91 { id: "facebook-access-token", label: "FACEBOOK_ACCESS_TOKEN", regex: "\\b(\\d{15,16}(\\||%)[0-9a-z\\-_]{27,40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: 3.0, keywords: ["facebook"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#facebook-access-token" },
92 { id: "facebook-page-access-token", label: "FACEBOOK_PAGE_ACCESS_TOKEN", regex: "\\b(EAA[MC][a-z0-9]{100,4096})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["eaam", "eaac"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#facebook-page-access-token" },
93 { id: "facebook-secret", label: "FACEBOOK_SECRET", regex: "[\\w.-]{0,50}?(?:facebook)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["facebook"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#facebook-secret" },
94 { id: "fastly-api-token", label: "FASTLY_API_TOKEN", regex: "[\\w.-]{0,50}?(?:fastly)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["fastly"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#fastly-api-token" },
95 { id: "finicity-api-token", label: "FINICITY_API_TOKEN", regex: "[\\w.-]{0,50}?(?:finicity)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["finicity"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#finicity-api-token" },
96 { id: "finicity-client-secret", label: "FINICITY_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:finicity)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["finicity"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#finicity-client-secret" },
97 { id: "finnhub-access-token", label: "FINNHUB_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:finnhub)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["finnhub"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#finnhub-access-token" },
98 { id: "flickr-access-token", label: "FLICKR_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:flickr)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["flickr"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#flickr-access-token" },
99 { id: "flutterwave-encryption-key", label: "FLUTTERWAVE_ENCRYPTION_KEY", regex: "FLWSECK_TEST-[a-h0-9]{12}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["flwseck_test"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#flutterwave-encryption-key" },
100 { id: "flutterwave-public-key", label: "FLUTTERWAVE_PUBLIC_KEY", regex: "FLWPUBK_TEST-[a-h0-9]{32}-X", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["flwpubk_test"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#flutterwave-public-key" },
101 { id: "flutterwave-secret-key", label: "FLUTTERWAVE_SECRET_KEY", regex: "FLWSECK_TEST-[a-h0-9]{32}-X", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["flwseck_test"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#flutterwave-secret-key" },
102 { id: "flyio-access-token", label: "FLYIO_ACCESS_TOKEN", regex: "\\b((?:fo1_[\\w-]{43}|fm1[ar]_[a-zA-Z0-9+\\/]{100,4096}={0,3}|fm2_[a-zA-Z0-9+\\/]{100,4096}={0,3}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["fo1_", "fm1", "fm2_"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#flyio-access-token" },
103 { id: "frameio-api-token", label: "FRAMEIO_API_TOKEN", regex: "fio-u-[a-z0-9\\-_=]{64}", flags: "i", groups: 0, secretGroup: null, entropy: null, keywords: ["fio-u-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#frameio-api-token" },
104 { id: "freemius-secret-key", label: "FREEMIUS_SECRET_KEY", regex: "[\"']secret_key[\"']\\s{0,4096}=>\\s{0,4096}[\"'](sk_[\\S]{29})[\"']", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["secret_key"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#freemius-secret-key" },
105 { id: "freshbooks-access-token", label: "FRESHBOOKS_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:freshbooks)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["freshbooks"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#freshbooks-access-token" },
106 { id: "gcp-api-key", label: "GCP_API_KEY", regex: "\\b(AIza[\\w-]{35})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["aiza"], allow: ["REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER", "REDACTED-GOOGLE-API-KEY-BY-SLOPSHOPPER"], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gcp-api-key" },
107 { id: "generic-api-key", label: "GENERIC_API_KEY", regex: "[\\w.-]{0,50}?(?:access|auth|(?:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([\\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,4096}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.5, keywords: ["access", "api", "auth", "key", "credential", "creds", "passwd", "password", "secret", "token"], allow: ["--mount=type=secret,"], kind: "generic", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#generic-api-key" },
108 { id: "github-app-token", label: "GITHUB_APP_TOKEN", regex: "(?:ghu|ghs)_[0-9a-zA-Z]{36}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["ghu_", "ghs_"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#github-app-token" },
109 { id: "github-fine-grained-pat", label: "GITHUB_FINE_GRAINED_PAT", regex: "github_pat_\\w{82}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["github_pat_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#github-fine-grained-pat" },
110 { id: "github-oauth", label: "GITHUB_OAUTH", regex: "gho_[0-9a-zA-Z]{36}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["gho_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#github-oauth" },
111 { id: "github-pat", label: "GITHUB_PAT", regex: "ghp_[0-9a-zA-Z]{36}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["ghp_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#github-pat" },
112 { id: "github-refresh-token", label: "GITHUB_REFRESH_TOKEN", regex: "ghr_[0-9a-zA-Z]{36}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["ghr_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#github-refresh-token" },
113 { id: "gitlab-cicd-job-token", label: "GITLAB_CICD_JOB_TOKEN", regex: "glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glcbt-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-cicd-job-token" },
114 { id: "gitlab-deploy-token", label: "GITLAB_DEPLOY_TOKEN", regex: "gldt-[0-9a-zA-Z_\\-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["gldt-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-deploy-token" },
115 { id: "gitlab-feature-flag-client-token", label: "GITLAB_FEATURE_FLAG_CLIENT_TOKEN", regex: "glffct-[0-9a-zA-Z_\\-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glffct-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-feature-flag-client-token" },
116 { id: "gitlab-feed-token", label: "GITLAB_FEED_TOKEN", regex: "glft-[0-9a-zA-Z_\\-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glft-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-feed-token" },
117 { id: "gitlab-incoming-mail-token", label: "GITLAB_INCOMING_MAIL_TOKEN", regex: "glimt-[0-9a-zA-Z_\\-]{25}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glimt-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-incoming-mail-token" },
118 { id: "gitlab-kubernetes-agent-token", label: "GITLAB_KUBERNETES_AGENT_TOKEN", regex: "glagent-[0-9a-zA-Z_\\-]{50}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glagent-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-kubernetes-agent-token" },
119 { id: "gitlab-oauth-app-secret", label: "GITLAB_OAUTH_APP_SECRET", regex: "gloas-[0-9a-zA-Z_\\-]{64}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["gloas-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-oauth-app-secret" },
120 { id: "gitlab-pat", label: "GITLAB_PAT", regex: "glpat-[\\w-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glpat-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-pat" },
121 { id: "gitlab-pat-routable", label: "GITLAB_PAT_ROUTABLE", regex: "\\bglpat-[0-9a-zA-Z_-]{27,300}\\.[0-9a-z]{2}[0-9a-z]{7}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 4.0, keywords: ["glpat-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-pat-routable" },
122 { id: "gitlab-ptt", label: "GITLAB_PTT", regex: "glptt-[0-9a-f]{40}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glptt-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-ptt" },
123 { id: "gitlab-rrt", label: "GITLAB_RRT", regex: "GR1348941[\\w-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["gr1348941"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-rrt" },
124 { id: "gitlab-runner-authentication-token", label: "GITLAB_RUNNER_AUTHENTICATION_TOKEN", regex: "glrt-[0-9a-zA-Z_\\-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glrt-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-runner-authentication-token" },
125 { id: "gitlab-runner-authentication-token-routable", label: "GITLAB_RUNNER_AUTHENTICATION_TOKEN_ROUTABLE", regex: "\\bglrt-t\\d_[0-9a-zA-Z_\\-]{27,300}\\.[0-9a-z]{2}[0-9a-z]{7}\\b", flags: "i", groups: 0, secretGroup: null, entropy: 4.0, keywords: ["glrt-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-runner-authentication-token-routable" },
126 { id: "gitlab-scim-token", label: "GITLAB_SCIM_TOKEN", regex: "glsoat-[0-9a-zA-Z_\\-]{20}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["glsoat-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-scim-token" },
127 { id: "gitlab-session-cookie", label: "GITLAB_SESSION_COOKIE", regex: "_gitlab_session=[0-9a-z]{32}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["_gitlab_session="], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitlab-session-cookie" },
128 { id: "gitter-access-token", label: "GITTER_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:gitter)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9_-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["gitter"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gitter-access-token" },
129 { id: "gocardless-api-token", label: "GOCARDLESS_API_TOKEN", regex: "[\\w.-]{0,50}?(?:gocardless)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(live_[a-z0-9\\-_=]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["live_", "gocardless"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#gocardless-api-token" },
130 { id: "grafana-api-key", label: "GRAFANA_API_KEY", regex: "\\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["eyjrijoi"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#grafana-api-key" },
131 { id: "grafana-cloud-api-token", label: "GRAFANA_CLOUD_API_TOKEN", regex: "\\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["glc_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#grafana-cloud-api-token" },
132 { id: "grafana-service-account-token", label: "GRAFANA_SERVICE_ACCOUNT_TOKEN", regex: "\\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["glsa_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#grafana-service-account-token" },
133 { id: "harness-api-key", label: "HARNESS_API_KEY", regex: "(?:pat|sat)\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9]{24}\\.[a-zA-Z0-9]{20}", flags: "i", groups: 0, secretGroup: null, entropy: null, keywords: ["pat.", "sat."], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#harness-api-key" },
134 { id: "hashicorp-tf-api-token", label: "HASHICORP_TF_API_TOKEN", regex: "[a-z0-9]{14}\\.(?:atlasv1)\\.[a-z0-9\\-_=]{60,70}", flags: "i", groups: 0, secretGroup: null, entropy: 3.5, keywords: ["atlasv1"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#hashicorp-tf-api-token" },
135 { id: "hashicorp-tf-password", label: "HASHICORP_TF_PASSWORD", regex: "[\\w.-]{0,50}?(?:administrator_login_password|password)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(\"[a-z0-9=_\\-]{8,20}\")(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["administrator_login_password", "password"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#hashicorp-tf-password" },
136 { id: "heroku-api-key", label: "HEROKU_API_KEY", regex: "[\\w.-]{0,50}?(?:heroku)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["heroku"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#heroku-api-key" },
137 { id: "heroku-api-key-v2", label: "HEROKU_API_KEY_V2", regex: "\\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: 4.0, keywords: ["hrku-aa"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#heroku-api-key-v2" },
138 { id: "hubspot-api-key", label: "HUBSPOT_API_KEY", regex: "[\\w.-]{0,50}?(?:hubspot)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["hubspot"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#hubspot-api-key" },
139 { id: "huggingface-access-token", label: "HUGGINGFACE_ACCESS_TOKEN", regex: "\\b(hf_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["hf_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#huggingface-access-token" },
140 { id: "huggingface-organization-api-token", label: "HUGGINGFACE_ORGANIZATION_API_TOKEN", regex: "\\b(api_org_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["api_org_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#huggingface-organization-api-token" },
141 { id: "infracost-api-token", label: "INFRACOST_API_TOKEN", regex: "\\b(ico-[a-zA-Z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["ico-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#infracost-api-token" },
142 { id: "intercom-api-key", label: "INTERCOM_API_KEY", regex: "[\\w.-]{0,50}?(?:intercom)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{60})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["intercom"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#intercom-api-key" },
143 { id: "intra42-client-secret", label: "INTRA42_CLIENT_SECRET", regex: "\\b(s-s4t2(?:ud|af)-[abcdef0123456789]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["intra", "s-s4t2ud-", "s-s4t2af-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#intra42-client-secret" },
144 { id: "jfrog-api-key", label: "JFROG_API_KEY", regex: "[\\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{73})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["jfrog", "artifactory", "bintray", "xray"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#jfrog-api-key" },
145 { id: "jfrog-identity-token", label: "JFROG_IDENTITY_TOKEN", regex: "[\\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["jfrog", "artifactory", "bintray", "xray"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#jfrog-identity-token" },
146 { id: "jwt", label: "JWT", regex: "\\b(ey[a-zA-Z0-9]{17,4096}\\.ey[a-zA-Z0-9\\/\\\\_-]{17,4096}\\.(?:[a-zA-Z0-9\\/\\\\_-]{10,4096}={0,2})?)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["ey"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#jwt" },
147 { id: "jwt-base64", label: "JWT_BASE64", regex: "\\bZXlK(?:(?<alg>aGJHY2lPaU)|(?<apu>aGNIVWlPaU)|(?<apv>aGNIWWlPaU)|(?<aud>aGRXUWlPaU)|(?<b64>aU5qUWlP)|(?<crit>amNtbDBJanBi)|(?<cty>amRIa2lPaU)|(?<epk>bGNHc2lPbn)|(?<enc>bGJtTWlPaU)|(?<jku>cWEzVWlPaU)|(?<jwk>cWQyc2lPb)|(?<iss>cGMzTWlPaU)|(?<iv>cGRpSTZJ)|(?<kid>cmFXUWlP)|(?<key_ops>clpYbGZiM0J6SWpwY)|(?<kty>cmRIa2lPaUp)|(?<nonce>dWIyNWpaU0k2)|(?<p2c>d01tTWlP)|(?<p2s>d01uTWlPaU)|(?<ppt>d2NIUWlPaU)|(?<sub>emRXSWlPaU)|(?<svt>emRuUWlP)|(?<tag>MFlXY2lPaU)|(?<typ>MGVYQWlPaUp)|(?<url>MWNtd2l)|(?<use>MWMyVWlPaUp)|(?<ver>MlpYSWlPaU)|(?<version>MlpYSnphVzl1SWpv)|(?<x>NElqb2)|(?<x5c>NE5XTWlP)|(?<x5t>NE5YUWlPaU)|(?<x5ts256>NE5YUWpVekkxTmlJNkl)|(?<x5u>NE5YVWlPaU)|(?<zip>NmFYQWlPaU))[a-zA-Z0-9\\/\\\\_+\\-\\r\\n]{40,4096}={0,2}", flags: "i", groups: 34, secretGroup: null, entropy: 2.0, keywords: ["zxlk"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#jwt-base64" },
148 { id: "kraken-access-token", label: "KRAKEN_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:kraken)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9\\/=_\\+\\-]{80,90})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["kraken"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#kraken-access-token" },
149 { id: "kubernetes-secret-yaml", label: "KUBERNETES_SECRET_YAML", regex: "(?:\\bkind:[ \\t]{0,4096}[\"']?\\bsecret\\b[\"']?[\\s\\S]{0,200}?\\bdata:[\\s\\S]{0,100}?\\s{1,4096}([\\w.-]{1,4096}:(?:[ \\t]{0,4096}(?:\\||>[-+]?)\\s{1,4096})?[ \\t]{0,4096}(?:[\"']?[a-z0-9+/]{10,4096}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]{1,4096}}}|\"\"|''))|\\bdata:[\\s\\S]{0,100}?\\s{1,4096}([\\w.-]{1,4096}:(?:[ \\t]{0,4096}(?:\\||>[-+]?)\\s{1,4096})?[ \\t]{0,4096}(?:[\"']?[a-z0-9+/]{10,4096}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]{1,4096}}}|\"\"|''))[\\s\\S]{0,200}?\\bkind:[ \\t]{0,4096}[\"']?\\bsecret\\b[\"']?)", flags: "i", groups: 2, secretGroup: null, entropy: null, keywords: ["secret"], allow: ["(kind:[\\s\\S]+\\n---\\n[\\s\\S]+\\bdata:|data:[\\s\\S]+\\n---\\n[\\s\\S]+\\bkind:)"], kind: "generic", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#kubernetes-secret-yaml" },
150 { id: "kucoin-access-token", label: "KUCOIN_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:kucoin)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["kucoin"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#kucoin-access-token" },
151 { id: "kucoin-secret-key", label: "KUCOIN_SECRET_KEY", regex: "[\\w.-]{0,50}?(?:kucoin)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["kucoin"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#kucoin-secret-key" },
152 { id: "launchdarkly-access-token", label: "LAUNCHDARKLY_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:launchdarkly)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["launchdarkly"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#launchdarkly-access-token" },
153 { id: "linear-api-key", label: "LINEAR_API_KEY", regex: "lin_api_[a-z0-9]{40}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["lin_api_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#linear-api-key" },
154 { id: "linear-client-secret", label: "LINEAR_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:linear)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["linear"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#linear-client-secret" },
155 { id: "linkedin-client-id", label: "LINKEDIN_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:linked[_-]?in)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{14})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["linkedin", "linked_in", "linked-in"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#linkedin-client-id" },
156 { id: "linkedin-client-secret", label: "LINKEDIN_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:linked[_-]?in)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["linkedin", "linked_in", "linked-in"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#linkedin-client-secret" },
157 { id: "lob-api-key", label: "LOB_API_KEY", regex: "[\\w.-]{0,50}?(?:lob)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}((live|test)_[a-f0-9]{35})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: null, keywords: ["test_", "live_"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#lob-api-key" },
158 { id: "lob-pub-api-key", label: "LOB_PUB_API_KEY", regex: "[\\w.-]{0,50}?(?:lob)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}((test|live)_pub_[a-f0-9]{31})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: null, keywords: ["test_pub", "live_pub", "_pub"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#lob-pub-api-key" },
159 { id: "looker-client-id", label: "LOOKER_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:looker)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["looker"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#looker-client-id" },
160 { id: "looker-client-secret", label: "LOOKER_CLIENT_SECRET", regex: "[\\w.-]{0,50}?(?:looker)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["looker"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#looker-client-secret" },
161 { id: "mailchimp-api-key", label: "MAILCHIMP_API_KEY", regex: "[\\w.-]{0,50}?(?:MailchimpSDK.initialize|mailchimp)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{32}-us\\d\\d)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["mailchimp"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#mailchimp-api-key" },
162 { id: "mailgun-private-api-token", label: "MAILGUN_PRIVATE_API_TOKEN", regex: "[\\w.-]{0,50}?(?:mailgun)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(key-[a-f0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["mailgun"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#mailgun-private-api-token" },
163 { id: "mailgun-pub-key", label: "MAILGUN_PUB_KEY", regex: "[\\w.-]{0,50}?(?:mailgun)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(pubkey-[a-f0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["mailgun"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#mailgun-pub-key" },
164 { id: "mailgun-signing-key", label: "MAILGUN_SIGNING_KEY", regex: "[\\w.-]{0,50}?(?:mailgun)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["mailgun"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#mailgun-signing-key" },
165 { id: "mapbox-api-token", label: "MAPBOX_API_TOKEN", regex: "[\\w.-]{0,50}?(?:mapbox)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(pk\\.[a-z0-9]{60}\\.[a-z0-9]{22})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["mapbox"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#mapbox-api-token" },
166 { id: "mattermost-access-token", label: "MATTERMOST_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:mattermost)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{26})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["mattermost"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#mattermost-access-token" },
167 { id: "maxmind-license-key", label: "MAXMIND_LICENSE_KEY", regex: "\\b([A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["_mmk"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#maxmind-license-key" },
168 { id: "messagebird-api-token", label: "MESSAGEBIRD_API_TOKEN", regex: "[\\w.-]{0,50}?(?:message[_-]?bird)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{25})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["messagebird", "message-bird", "message_bird"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#messagebird-api-token" },
169 { id: "messagebird-client-id", label: "MESSAGEBIRD_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:message[_-]?bird)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["messagebird", "message-bird", "message_bird"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#messagebird-client-id" },
170 { id: "microsoft-teams-webhook", label: "MICROSOFT_TEAMS_WEBHOOK", regex: "https://[a-z0-9]{1,4096}\\.webhook\\.office\\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}", flags: "i", groups: 3, secretGroup: null, entropy: null, keywords: ["webhook.office.com", "webhookb2", "incomingwebhook"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#microsoft-teams-webhook" },
171 { id: "netlify-access-token", label: "NETLIFY_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:netlify)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{40,46})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["netlify"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#netlify-access-token" },
172 { id: "new-relic-browser-api-token", label: "NEW_RELIC_BROWSER_API_TOKEN", regex: "[\\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(NRJS-[a-f0-9]{19})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["nrjs-"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#new-relic-browser-api-token" },
173 { id: "new-relic-insert-key", label: "NEW_RELIC_INSERT_KEY", regex: "[\\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(NRII-[a-z0-9-]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["nrii-"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#new-relic-insert-key" },
174 { id: "new-relic-user-api-id", label: "NEW_RELIC_USER_API_ID", regex: "[\\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["new-relic", "newrelic", "new_relic"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#new-relic-user-api-id" },
175 { id: "new-relic-user-api-key", label: "NEW_RELIC_USER_API_KEY", regex: "[\\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(NRAK-[a-z0-9]{27})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["nrak"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#new-relic-user-api-key" },
176 { id: "notion-api-token", label: "NOTION_API_TOKEN", regex: "\\b(ntn_[0-9]{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["ntn_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#notion-api-token" },
177 { id: "npm-access-token", label: "NPM_ACCESS_TOKEN", regex: "\\b(npm_[a-z0-9]{36})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["npm_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#npm-access-token" },
178 { id: "nuget-config-password", label: "NUGET_CONFIG_PASSWORD", regex: "<add key=\\\"(?:(?:ClearText)?Password)\\\"\\s{0,4096}value=\\\"(.{8,4096})\\\"\\s{0,4096}/>", flags: "i", groups: 1, secretGroup: null, entropy: 1.0, keywords: ["<add key="], allow: ["33f!!lloppa", "hal\\+9ooo_da!sY", "^\\%\\S.*\\%$"], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#nuget-config-password" },
179 { id: "nytimes-access-token", label: "NYTIMES_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:nytimes|new-york-times,|newyorktimes)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9=_\\-]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["nytimes", "new-york-times", "newyorktimes"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#nytimes-access-token" },
180 { id: "octopus-deploy-api-key", label: "OCTOPUS_DEPLOY_API_KEY", regex: "\\b(API-[A-Z0-9]{26})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["api-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#octopus-deploy-api-key" },
181 { id: "okta-access-token", label: "OKTA_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Oo]kta|OKTA))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(00[\\w=\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["okta"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#okta-access-token" },
182 { id: "openai-api-key", label: "OPENAI_API_KEY", regex: "\\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["t3blbkfj"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#openai-api-key" },
183 { id: "openshift-user-token", label: "OPENSHIFT_USER_TOKEN", regex: "\\b(sha256~[\\w-]{43})(?:[^\\w-]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.5, keywords: ["sha256~"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#openshift-user-token" },
184 { id: "perplexity-api-key", label: "PERPLEXITY_API_KEY", regex: "\\b(pplx-[a-zA-Z0-9]{48})(?:[\\x60'\"\\s;]|\\\\[nr]|$|\\b)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["pplx-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#perplexity-api-key" },
185 { id: "plaid-api-token", label: "PLAID_API_TOKEN", regex: "[\\w.-]{0,50}?(?:plaid)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(access-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["plaid"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#plaid-api-token" },
186 { id: "plaid-client-id", label: "PLAID_CLIENT_ID", regex: "[\\w.-]{0,50}?(?:plaid)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.5, keywords: ["plaid"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#plaid-client-id" },
187 { id: "plaid-secret-key", label: "PLAID_SECRET_KEY", regex: "[\\w.-]{0,50}?(?:plaid)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{30})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.5, keywords: ["plaid"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#plaid-secret-key" },
188 { id: "planetscale-api-token", label: "PLANETSCALE_API_TOKEN", regex: "\\b(pscale_tkn_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["pscale_tkn_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#planetscale-api-token" },
189 { id: "planetscale-oauth-token", label: "PLANETSCALE_OAUTH_TOKEN", regex: "\\b(pscale_oauth_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["pscale_oauth_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#planetscale-oauth-token" },
190 { id: "planetscale-password", label: "PLANETSCALE_PASSWORD", regex: "\\b(pscale_pw_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["pscale_pw_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#planetscale-password" },
191 { id: "postman-api-token", label: "POSTMAN_API_TOKEN", regex: "\\b(PMAK-[a-f0-9]{24}\\-[a-f0-9]{34})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["pmak-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#postman-api-token" },
192 { id: "prefect-api-token", label: "PREFECT_API_TOKEN", regex: "\\b(pnu_[a-zA-Z0-9]{36})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["pnu_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#prefect-api-token" },
193 { id: "privateai-api-token", label: "PRIVATEAI_API_TOKEN", regex: "[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:private[_-]?ai)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["privateai", "private_ai", "private-ai"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#privateai-api-token" },
194 { id: "pulumi-api-token", label: "PULUMI_API_TOKEN", regex: "\\b(pul-[a-f0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["pul-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#pulumi-api-token" },
195 { id: "pypi-upload-token", label: "PYPI_UPLOAD_TOKEN", regex: "pypi-AgEIcHlwaS5vcmc[\\w-]{50,1000}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["pypi-ageichlwas5vcmc"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#pypi-upload-token" },
196 { id: "rapidapi-access-token", label: "RAPIDAPI_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:rapidapi)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9_-]{50})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["rapidapi"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#rapidapi-access-token" },
197 { id: "readme-api-token", label: "README_API_TOKEN", regex: "\\b(rdme_[a-z0-9]{70})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["rdme_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#readme-api-token" },
198 { id: "rubygems-api-token", label: "RUBYGEMS_API_TOKEN", regex: "\\b(rubygems_[a-f0-9]{48})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["rubygems_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#rubygems-api-token" },
199 { id: "scalingo-api-token", label: "SCALINGO_API_TOKEN", regex: "\\b(tk-us-[\\w-]{48})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["tk-us-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#scalingo-api-token" },
200 { id: "sendbird-access-id", label: "SENDBIRD_ACCESS_ID", regex: "[\\w.-]{0,50}?(?:sendbird)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["sendbird"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sendbird-access-id" },
201 { id: "sendbird-access-token", label: "SENDBIRD_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:sendbird)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["sendbird"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sendbird-access-token" },
202 { id: "sendgrid-api-token", label: "SENDGRID_API_TOKEN", regex: "\\b(SG\\.[a-z0-9=_\\-\\.]{66})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["sg."], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sendgrid-api-token" },
203 { id: "sendinblue-api-token", label: "SENDINBLUE_API_TOKEN", regex: "\\b(xkeysib-[a-f0-9]{64}\\-[a-z0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["xkeysib-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sendinblue-api-token" },
204 { id: "sentry-access-token", label: "SENTRY_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:sentry)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["sentry"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sentry-access-token" },
205 { id: "sentry-org-token", label: "SENTRY_ORG_TOKEN", regex: "\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|$)", flags: "i", groups: 0, secretGroup: null, entropy: 4.5, keywords: ["sntrys_eyjpyxqio"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sentry-org-token" },
206 { id: "sentry-user-token", label: "SENTRY_USER_TOKEN", regex: "\\b(sntryu_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.5, keywords: ["sntryu_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sentry-user-token" },
207 { id: "settlemint-application-access-token", label: "SETTLEMINT_APPLICATION_ACCESS_TOKEN", regex: "\\b(sm_aat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["sm_aat"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#settlemint-application-access-token" },
208 { id: "settlemint-personal-access-token", label: "SETTLEMINT_PERSONAL_ACCESS_TOKEN", regex: "\\b(sm_pat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["sm_pat"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#settlemint-personal-access-token" },
209 { id: "settlemint-service-access-token", label: "SETTLEMINT_SERVICE_ACCESS_TOKEN", regex: "\\b(sm_sat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["sm_sat"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#settlemint-service-access-token" },
210 { id: "shippo-api-token", label: "SHIPPO_API_TOKEN", regex: "\\b(shippo_(?:live|test)_[a-fA-F0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["shippo_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#shippo-api-token" },
211 { id: "shopify-access-token", label: "SHOPIFY_ACCESS_TOKEN", regex: "shpat_[a-fA-F0-9]{32}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["shpat_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#shopify-access-token" },
212 { id: "shopify-custom-access-token", label: "SHOPIFY_CUSTOM_ACCESS_TOKEN", regex: "shpca_[a-fA-F0-9]{32}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["shpca_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#shopify-custom-access-token" },
213 { id: "shopify-private-app-access-token", label: "SHOPIFY_PRIVATE_APP_ACCESS_TOKEN", regex: "shppa_[a-fA-F0-9]{32}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["shppa_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#shopify-private-app-access-token" },
214 { id: "shopify-shared-secret", label: "SHOPIFY_SHARED_SECRET", regex: "shpss_[a-fA-F0-9]{32}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["shpss_"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#shopify-shared-secret" },
215 { id: "sidekiq-secret", label: "SIDEKIQ_SECRET", regex: "[\\w.-]{0,50}?(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["bundle_enterprise__contribsys__com", "bundle_gems__contribsys__com"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sidekiq-secret" },
216 { id: "sidekiq-sensitive-url", label: "SIDEKIQ_SENSITIVE_URL", regex: "\\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\\/|\\#|\\?|:]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["gems.contribsys.com", "enterprise.contribsys.com"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sidekiq-sensitive-url" },
217 { id: "slack-app-token", label: "SLACK_APP_TOKEN", regex: "xapp-\\d-[A-Z0-9]{1,4096}-\\d{1,4096}-[a-z0-9]{1,4096}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xapp"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-app-token" },
218 { id: "slack-bot-token", label: "SLACK_BOT_TOKEN", regex: "xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]{0,4096}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["xoxb"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-bot-token" },
219 { id: "slack-config-access-token", label: "SLACK_CONFIG_ACCESS_TOKEN", regex: "xoxe.xox[bp]-\\d-[A-Z0-9]{163,166}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xoxe.xoxb-", "xoxe.xoxp-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-config-access-token" },
220 { id: "slack-config-refresh-token", label: "SLACK_CONFIG_REFRESH_TOKEN", regex: "xoxe-\\d-[A-Z0-9]{146}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xoxe-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-config-refresh-token" },
221 { id: "slack-legacy-bot-token", label: "SLACK_LEGACY_BOT_TOKEN", regex: "xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xoxb"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-legacy-bot-token" },
222 { id: "slack-legacy-token", label: "SLACK_LEGACY_TOKEN", regex: "xox[os]-\\d{1,4096}-\\d{1,4096}-\\d{1,4096}-[a-fA-F\\d]{1,4096}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xoxo", "xoxs"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-legacy-token" },
223 { id: "slack-legacy-workspace-token", label: "SLACK_LEGACY_WORKSPACE_TOKEN", regex: "xox[ar]-(?:\\d-)?[0-9a-zA-Z]{8,48}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xoxa", "xoxr"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-legacy-workspace-token" },
224 { id: "slack-user-token", label: "SLACK_USER_TOKEN", regex: "xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}", flags: "i", groups: 0, secretGroup: null, entropy: 2.0, keywords: ["xoxp-", "xoxe-"], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-user-token" },
225 { id: "slack-webhook-url", label: "SLACK_WEBHOOK_URL", regex: "(?:https?://)?hooks.slack.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56}", flags: "i", groups: 0, secretGroup: null, entropy: null, keywords: ["hooks.slack.com"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#slack-webhook-url" },
226 { id: "snyk-api-token", label: "SNYK_API_TOKEN", regex: "[\\w.-]{0,50}?(?:snyk[_.-]?(?:(?:api|oauth)[_.-]?)?(?:key|token))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["snyk"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#snyk-api-token" },
227 { id: "sonar-api-token", label: "SONAR_API_TOKEN", regex: "[\\w.-]{0,50}?(?:sonar[_.-]?(login|token))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}((?:squ_|sqp_|sqa_)?[a-z0-9=_\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: 2, entropy: null, keywords: ["sonar"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sonar-api-token" },
228 { id: "sourcegraph-access-token", label: "SOURCEGRAPH_ACCESS_TOKEN", regex: "\\b(\\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40}|[a-fA-F0-9]{40})\\b)(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 2, secretGroup: null, entropy: 3.0, keywords: ["sgp_", "sourcegraph"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sourcegraph-access-token" },
229 { id: "square-access-token", label: "SQUARE_ACCESS_TOKEN", regex: "\\b((?:EAAA|sq0atp-)[\\w-]{22,60})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["sq0atp-", "eaaa"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#square-access-token" },
230 { id: "squarespace-access-token", label: "SQUARESPACE_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:squarespace)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["squarespace"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#squarespace-access-token" },
231 { id: "stripe-access-token", label: "STRIPE_ACCESS_TOKEN", regex: "\\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 2.0, keywords: ["sk_test", "sk_live", "sk_prod", "rk_test", "rk_live", "rk_prod"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#stripe-access-token" },
232 { id: "sumologic-access-id", label: "SUMOLOGIC_ACCESS_ID", regex: "[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["sumo"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sumologic-access-id" },
233 { id: "sumologic-access-token", label: "SUMOLOGIC_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:(?:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.0, keywords: ["sumo"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#sumologic-access-token" },
234 { id: "telegram-bot-api-token", label: "TELEGRAM_BOT_API_TOKEN", regex: "[\\w.-]{0,50}?(?:telegr)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{5,16}:(?:A)[a-z0-9_\\-]{34})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["telegr"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#telegram-bot-api-token" },
235 { id: "travisci-access-token", label: "TRAVISCI_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:travis)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{22})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["travis"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#travisci-access-token" },
236 { id: "twilio-api-key", label: "TWILIO_API_KEY", regex: "SK[0-9a-fA-F]{32}", flags: "i", groups: 0, secretGroup: null, entropy: 3.0, keywords: ["sk"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twilio-api-key" },
237 { id: "twitch-api-token", label: "TWITCH_API_TOKEN", regex: "[\\w.-]{0,50}?(?:twitch)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{30})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["twitch"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twitch-api-token" },
238 { id: "twitter-access-secret", label: "TWITTER_ACCESS_SECRET", regex: "[\\w.-]{0,50}?(?:twitter)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{45})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["twitter"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twitter-access-secret" },
239 { id: "twitter-access-token", label: "TWITTER_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:twitter)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{15,25}-[a-zA-Z0-9]{20,40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["twitter"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twitter-access-token" },
240 { id: "twitter-api-key", label: "TWITTER_API_KEY", regex: "[\\w.-]{0,50}?(?:twitter)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{25})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["twitter"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twitter-api-key" },
241 { id: "twitter-api-secret", label: "TWITTER_API_SECRET", regex: "[\\w.-]{0,50}?(?:twitter)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{50})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["twitter"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twitter-api-secret" },
242 { id: "twitter-bearer-token", label: "TWITTER_BEARER_TOKEN", regex: "[\\w.-]{0,50}?(?:twitter)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(A{22}[a-zA-Z0-9%]{80,100})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["twitter"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#twitter-bearer-token" },
243 { id: "typeform-api-token", label: "TYPEFORM_API_TOKEN", regex: "[\\w.-]{0,50}?(?:typeform)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(tfp_[a-z0-9\\-_\\.=]{59})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["tfp_"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#typeform-api-token" },
244 { id: "vault-batch-token", label: "VAULT_BATCH_TOKEN", regex: "\\b(hvb\\.[\\w-]{138,300})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 4.0, keywords: ["hvb."], allow: [], kind: "prefix", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#vault-batch-token" },
245 { id: "vault-service-token", label: "VAULT_SERVICE_TOKEN", regex: "\\b((?:hvs\\.[\\w-]{90,120}|s\\.(?:[a-z0-9]{24})))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: 3.5, keywords: ["hvs.", "s."], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#vault-service-token" },
246 { id: "yandex-access-token", label: "YANDEX_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:yandex)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(t1\\.[A-Z0-9a-z_-]{1,4096}[=]{0,2}\\.[A-Z0-9a-z_-]{86}[=]{0,2})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["yandex"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#yandex-access-token" },
247 { id: "yandex-api-key", label: "YANDEX_API_KEY", regex: "[\\w.-]{0,50}?(?:yandex)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(AQVN[A-Za-z0-9_\\-]{35,38})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["yandex"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#yandex-api-key" },
248 { id: "yandex-aws-access-token", label: "YANDEX_AWS_ACCESS_TOKEN", regex: "[\\w.-]{0,50}?(?:yandex)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(YC[a-zA-Z0-9_\\-]{38})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["yandex"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#yandex-aws-access-token" },
249 { id: "zendesk-secret-key", label: "ZENDESK_SECRET_KEY", regex: "[\\w.-]{0,50}?(?:zendesk)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", flags: "i", groups: 1, secretGroup: null, entropy: null, keywords: ["zendesk"], allow: [], kind: "vendor-keyword", source: "https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#zendesk-secret-key" },
250];
251
252/** gitleaks' own global allowlist, minus its path patterns (rows carry no path). */
253export const GLOBAL_ALLOW_REGEXES: readonly string[] = [
254 "^true|false|null$",
255 "^(?:a+|b+|c+|d+|e+|f+|g+|h+|i+|j+|k+|l+|m+|n+|o+|p+|q+|r+|s+|t+|u+|v+|w+|x+|y+|z+|\\*+|\\.+)$",
256 "^\\$(?:\\d+|{\\d+})$",
257];
258
259export const GLOBAL_STOPWORDS: readonly string[] = [
260 "014df517-39d1-4453-b7b3-9930c563627c",
261 "abcdefghijklmnopqrstuvwxyz",
262];
263
264export const CORPUS_SHA256 = "e163e53b9e7e8a8511e77271e2b323ed057759542a6d988258afe3a1fa329caf";
265types/index.d.ts 34 lines1declare module "claude-code" {
2 interface PluginState {
3 "prompt-redact": {
4 /** Per-session salt: a placeholder minted under one salt means nothing under another. */
5 salt: string;
6 /** Placeholder -> real value. Session-scoped; never written to disk or $.store. */
7 vault: Record<string, string>;
8 /** Label -> how many values of that label were hidden this session. */
9 counts: Record<string, number>;
10 /** How many placeholders were restored into Edit/Write/NotebookEdit arguments. */
11 restored: number;
12 /** Scanning on/off for this session (/redact on|off). */
13 enabled: boolean;
14 /** Aggressiveness preset. */
15 tier: "strict" | "balanced" | "precision";
16 /** User allowlist: candidate values matching are never redacted. */
17 allow: string[];
18 /** User force-redact list: values matching are always redacted. */
19 deny: string[];
20 /** Rule ids switched off for this session; a disabled rule never fires. */
21 disabled: string[];
22 /** Whether the opaque-placeholder notice already landed on this prompt. */
23 noticeShown: boolean;
24 /**
25 * How many runs of each interception hook reached this code this session,
26 * by event name. Runs the engine or an outer plugin skipped are absent:
27 * the platform's skip notice is written outside this process, so a count
28 * is an observation, never proof of coverage.
29 */
30 hookFires: Record<string, number>;
31 };
32 }
33}
34