finitemonkey 0.1.0-next.18 → 0.1.0-next.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.md CHANGED
@@ -146,6 +146,7 @@ Generado desde `src/core/footprint.ts`, la lista que los tests exigen al código
146
146
  | Escribe | `.finitemonkey/reports/` | Informes de error enmascarados que escribe `doctor --report`, solo cuando se pide. |
147
147
  | Envía | `api.github.com` | A través de `gh`, solo si está instalado y con sesión iniciada: el estado del pull request indicado en una tarea y tus pull requests abiertos para la línea de la sesión. El código nunca se sube. |
148
148
  | Envía | `127.0.0.1` | Una petición a tu propia app en el puerto que declara la comprobación `runtime`, después de arrancarla o encontrarla en marcha. Nada sale de esta máquina. |
149
+ | Envía | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | A través de `curl`, solo cuando tú ejecutas `finitemonkey secrets live <proveedor> --env <VARIABLE> --send`: una petición con esa clave a la API del proveedor para ver si aún funciona. Apagado por defecto, nunca lo ejecuta un hook, y el host se muestra antes de enviar nada. |
149
150
  <!-- generated:footprint:end -->
150
151
 
151
152
  ## Preguntas frecuentes
package/README.md CHANGED
@@ -146,6 +146,7 @@ Generated from `src/core/footprint.ts`, the list the tests hold the code to. No
146
146
  | Writes | `.finitemonkey/reports/` | Masked bug reports that `doctor --report` writes, only when asked. |
147
147
  | Sends | `api.github.com` | Through `gh`, only when it is installed and logged in: the state of the pull request named in a task, and your open pull requests for the session line. Code is never uploaded. |
148
148
  | Sends | `127.0.0.1` | One request to your own app on the port a `runtime` check declares, after starting it or finding it running. Nothing goes past this machine. |
149
+ | Sends | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | Through `curl`, only when you run `finitemonkey secrets live <provider> --env <VARIABLE> --send` yourself: one request with that key to the provider's API to see whether it still works. Off by default, never run by a hook, and the host is printed before anything is sent. |
149
150
  <!-- generated:footprint:end -->
150
151
 
151
152
  ## FAQ
package/README.pt-BR.md CHANGED
@@ -146,6 +146,7 @@ Gerado a partir de `src/core/footprint.ts`, a lista que os testes cobram do cód
146
146
  | Grava | `.finitemonkey/reports/` | Relatórios de bug mascarados que o `doctor --report` grava, só quando pedido. |
147
147
  | Envia | `api.github.com` | Pelo `gh`, só quando ele está instalado e logado: o estado do pull request citado numa tarefa e os seus pull requests abertos para a linha da sessão. Código nunca é enviado. |
148
148
  | Envia | `127.0.0.1` | Uma requisição ao seu próprio app na porta que a checagem `runtime` declara, depois de subir o app ou achá-lo rodando. Nada sai desta máquina. |
149
+ | Envia | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | Pelo `curl`, só quando você roda `finitemonkey secrets live <provedor> --env <VARIAVEL> --send`: uma requisição com aquela chave para a API do provedor para ver se ela ainda funciona. Desligado por padrão, nunca roda por hook, e o host aparece antes de qualquer envio. |
149
150
  <!-- generated:footprint:end -->
150
151
 
151
152
  ## Perguntas frequentes
@@ -1156,8 +1156,8 @@ var isOwnHandler = (handler) => {
1156
1156
  if (args2.length === 0) return OWN_SHELL.test(`${command} `);
1157
1157
  const hookAt = args2.indexOf("hook");
1158
1158
  if (hookAt < 0) return false;
1159
- const target = hookAt === 0 ? command : args2[hookAt - 1] ?? "";
1160
- return OWN_SCRIPT.test(target);
1159
+ const target2 = hookAt === 0 ? command : args2[hookAt - 1] ?? "";
1160
+ return OWN_SCRIPT.test(target2);
1161
1161
  };
1162
1162
 
1163
1163
  // src/install/skill-template.ts
@@ -3356,8 +3356,8 @@ var readRedirect = (cursor, match) => {
3356
3356
  cursor.pos += match[0].length;
3357
3357
  skipBlank(cursor);
3358
3358
  const processSub = peek(cursor) === "(" && op === "<";
3359
- const target = processSub ? "" : readWord(cursor).text;
3360
- return { kind: "redirect", op, fd: match[1] ?? "", target, body: void 0 };
3359
+ const target2 = processSub ? "" : readWord(cursor).text;
3360
+ return { kind: "redirect", op, fd: match[1] ?? "", target: target2, body: void 0 };
3361
3361
  };
3362
3362
  var processSubstitution = (cursor) => {
3363
3363
  if (peek(cursor) !== "<" && peek(cursor) !== ">" || peek(cursor, 1) !== "(") return false;
@@ -3536,8 +3536,8 @@ var next = (cursor, tokens, cmd) => {
3536
3536
  if (redirect !== null) {
3537
3537
  cursor.pos += redirect[0].length;
3538
3538
  while (peek(cursor) === " ") cursor.pos += 1;
3539
- const target = redirect[3] ?? word(cursor, cmd).text;
3540
- tokens.push({ kind: "redirect", op: redirect[2] ?? ">", fd: redirect[1] ?? "", target, body: void 0 });
3539
+ const target2 = redirect[3] ?? word(cursor, cmd).text;
3540
+ tokens.push({ kind: "redirect", op: redirect[2] ?? ">", fd: redirect[1] ?? "", target: target2, body: void 0 });
3541
3541
  return;
3542
3542
  }
3543
3543
  const op = OPERATORS2.find((candidate) => cursor.text.startsWith(candidate, cursor.pos));
@@ -3759,8 +3759,8 @@ var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
3759
3759
  var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
3760
3760
  var lex = (text6, language) => language === "bash" ? lexBash(text6) : lexPowerShell(text6, language === "cmd");
3761
3761
  var isWindowsAbsolute = (path) => /^[A-Za-z]:\//.test(path);
3762
- var joinDir = (current2, target) => {
3763
- const next2 = toPosixPath(target);
3762
+ var joinDir = (current2, target2) => {
3763
+ const next2 = toPosixPath(target2);
3764
3764
  if (next2.startsWith("/") || isWindowsAbsolute(next2) || next2.startsWith("~")) return next2;
3765
3765
  return current2 === void 0 ? posix.normalize(next2) : posix.join(current2, next2);
3766
3766
  };
@@ -3785,8 +3785,8 @@ var track = (words, language, context) => {
3785
3785
  if (assigned !== void 0) context.env = { ...context.env, ...assigned };
3786
3786
  const program = language === "bash" ? words[0] ?? "" : powershellProgram(words[0] ?? "");
3787
3787
  if (!CHANGES_DIR.has(program)) return;
3788
- const target = words.slice(1).find((word2) => !word2.startsWith("-"));
3789
- context.dir = target === void 0 || target === "-" ? "~" : joinDir(context.dir, target);
3788
+ const target2 = words.slice(1).find((word2) => !word2.startsWith("-"));
3789
+ context.dir = target2 === void 0 || target2 === "-" ? "~" : joinDir(context.dir, target2);
3790
3790
  };
3791
3791
  var emit = (stage, raw, language, context, collector) => {
3792
3792
  const first = stage.argv[0] ?? "";
@@ -4295,15 +4295,15 @@ var NOT_FROM_PERSON = [
4295
4295
  /\[Subagent hand-back\]/i
4296
4296
  ];
4297
4297
  var comesFromPerson = (prompt) => !NOT_FROM_PERSON.some((pattern) => pattern.test(prompt));
4298
- var toMarker = (verb, target, rest) => {
4299
- if (!NAME.test(target)) return void 0;
4300
- if (verb === "trust") return { type: "trust", task: target };
4298
+ var toMarker = (verb, target2, rest) => {
4299
+ if (!NAME.test(target2)) return void 0;
4300
+ if (verb === "trust") return { type: "trust", task: target2 };
4301
4301
  if (verb === "allow") {
4302
- return rest.length >= 3 ? { type: "allow", guard: target.toLowerCase(), reason: rest } : void 0;
4302
+ return rest.length >= 3 ? { type: "allow", guard: target2.toLowerCase(), reason: rest } : void 0;
4303
4303
  }
4304
- const slash = target.indexOf("/");
4305
- const task = slash < 0 ? void 0 : target.slice(0, slash);
4306
- const check2 = slash < 0 ? target : target.slice(slash + 1);
4304
+ const slash = target2.indexOf("/");
4305
+ const task = slash < 0 ? void 0 : target2.slice(0, slash);
4306
+ const check2 = slash < 0 ? target2 : target2.slice(slash + 1);
4307
4307
  if (check2 === "") return void 0;
4308
4308
  return { type: "approve", check: check2, reason: rest, ...task === void 0 || task === "" ? {} : { task } };
4309
4309
  };
@@ -5191,9 +5191,9 @@ var prStatus = (runtime, git, paths, ttlMs = PR_CACHE_TTL_MS) => {
5191
5191
  return { kind: "found", branch, pr: status.pr, localHead: headSha(git) };
5192
5192
  };
5193
5193
  var conflictFiles = (git, base) => {
5194
- for (const target of [`origin/${base}`, base]) {
5195
- if (git.out(["rev-parse", "--verify", "-q", `${target}^{commit}`]) === void 0) continue;
5196
- const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target, "HEAD"]);
5194
+ for (const target2 of [`origin/${base}`, base]) {
5195
+ if (git.out(["rev-parse", "--verify", "-q", `${target2}^{commit}`]) === void 0) continue;
5196
+ const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target2, "HEAD"]);
5197
5197
  if (result.found && result.exitCode === 1) return parseMergeTreeConflicts(result.stdout);
5198
5198
  if (result.found && result.exitCode === 0) return [];
5199
5199
  }
@@ -6196,7 +6196,17 @@ var en_default = {
6196
6196
  "pause.on": "Gate resumed.",
6197
6197
  "pause.report": "The gate did not check this stop: the person paused it with fm off at {since}.",
6198
6198
  "pause.no_session": "No Claude Code session is recorded for this project yet; start one, then run this again or type `fm off` there.",
6199
- "pause.error.person_only": "Only the person can pause or resume the gate, and this does not look like their terminal (no TTY, or running inside Claude Code)"
6199
+ "pause.error.person_only": "Only the person can pause or resume the gate, and this does not look like their terminal (no TTY, or running inside Claude Code)",
6200
+ "secrets.incident.runbook": " runbook: {link}",
6201
+ "secrets.command.live": "Check whether a key still works with its provider (off by default; sends only with --send, and prints the host first)",
6202
+ "secrets.action.live": "send a key to its provider",
6203
+ "secrets.live.dry": "Nothing was sent. With --send, the value of ${variable} would go to {host} ({url}) to check whether the {provider} key still works. Run it yourself, in your own terminal, with --send.",
6204
+ "secrets.live.contacting": "Contacting {host} ({url}) with the key; the value is passed to curl on stdin, never as an argument.",
6205
+ "secrets.live.live": "The key in ${variable} still works at {host} (HTTP {status}). Revoke it now: see the runbook in `finitemonkey secrets incident`.",
6206
+ "secrets.live.rejected": "{host} rejected the key in ${variable} (HTTP {status}): it no longer works.",
6207
+ "secrets.live.unknown": "{host} answered HTTP {status}; that does not say whether the key in ${variable} works. Check it in the provider's console.",
6208
+ "secrets.live.unreachable": "Could not reach {host} with curl, so the key in ${variable} was not checked.",
6209
+ "secrets.live.no_value": "${variable} is empty or not set in this terminal; export it first, nothing was sent"
6200
6210
  };
6201
6211
 
6202
6212
  // src/i18n/es.json
@@ -6757,7 +6767,17 @@ var es_default = {
6757
6767
  "pause.on": "Compuerta reanudada.",
6758
6768
  "pause.report": "La compuerta no revisó esta parada: la persona la pausó con fm off a las {since}.",
6759
6769
  "pause.no_session": "Todavía no hay una sesión de Claude Code registrada en este proyecto; abre una y vuelve a ejecutarlo, o escribe `fm off` allí.",
6760
- "pause.error.person_only": "Solo la persona puede pausar o reanudar la compuerta, y esto no parece su terminal (sin TTY, o dentro de Claude Code)"
6770
+ "pause.error.person_only": "Solo la persona puede pausar o reanudar la compuerta, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
6771
+ "secrets.incident.runbook": " guía: {link}",
6772
+ "secrets.command.live": "Comprueba si una clave aún funciona en su proveedor (apagado por defecto; solo envía con --send y muestra el host antes)",
6773
+ "secrets.action.live": "enviar una clave a su proveedor",
6774
+ "secrets.live.dry": "No se envió nada. Con --send, el valor de ${variable} iría a {host} ({url}) para comprobar si la clave de {provider} aún funciona. Ejecútalo tú, en tu terminal, con --send.",
6775
+ "secrets.live.contacting": "Contactando {host} ({url}) con la clave; el valor va a curl por la entrada estándar, nunca como argumento.",
6776
+ "secrets.live.live": "La clave en ${variable} aún funciona en {host} (HTTP {status}). Revócala ya: mira la guía en `finitemonkey secrets incident`.",
6777
+ "secrets.live.rejected": "{host} rechazó la clave en ${variable} (HTTP {status}): ya no funciona.",
6778
+ "secrets.live.unknown": "{host} respondió HTTP {status}; eso no dice si la clave en ${variable} funciona. Revísala en la consola del proveedor.",
6779
+ "secrets.live.unreachable": "No se pudo llegar a {host} con curl, así que la clave en ${variable} no se comprobó.",
6780
+ "secrets.live.no_value": "${variable} está vacía o no existe en esta terminal; expórtala antes, no se envió nada"
6761
6781
  };
6762
6782
 
6763
6783
  // src/i18n/pt-BR.json
@@ -7318,7 +7338,17 @@ var pt_BR_default = {
7318
7338
  "pause.on": "Portão retomado.",
7319
7339
  "pause.report": "O portão não conferiu esta parada: a pessoa pausou com fm off em {since}.",
7320
7340
  "pause.no_session": "Ainda não há sessão do Claude Code registrada neste projeto; abra uma e rode de novo, ou digite `fm off` nela.",
7321
- "pause.error.person_only": "Só a pessoa pode pausar ou retomar o portão, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)"
7341
+ "pause.error.person_only": "Só a pessoa pode pausar ou retomar o portão, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
7342
+ "secrets.incident.runbook": " passo a passo: {link}",
7343
+ "secrets.command.live": "Verifica se uma chave ainda funciona no provedor (desligado por padrão; só envia com --send e mostra o host antes)",
7344
+ "secrets.action.live": "enviar uma chave ao provedor",
7345
+ "secrets.live.dry": "Nada foi enviado. Com --send, o valor de ${variable} iria para {host} ({url}) para ver se a chave {provider} ainda funciona. Rode você mesmo, no seu terminal, com --send.",
7346
+ "secrets.live.contacting": "Contatando {host} ({url}) com a chave; o valor vai para o curl pela entrada padrão, nunca como argumento.",
7347
+ "secrets.live.live": "A chave em ${variable} ainda funciona em {host} (HTTP {status}). Revogue agora: veja o passo a passo em `finitemonkey secrets incident`.",
7348
+ "secrets.live.rejected": "{host} recusou a chave em ${variable} (HTTP {status}): ela não funciona mais.",
7349
+ "secrets.live.unknown": "{host} respondeu HTTP {status}; isso não diz se a chave em ${variable} funciona. Confira no painel do provedor.",
7350
+ "secrets.live.unreachable": "Não deu para alcançar {host} com o curl, então a chave em ${variable} não foi verificada.",
7351
+ "secrets.live.no_value": "${variable} está vazia ou não existe neste terminal; exporte antes, nada foi enviado"
7322
7352
  };
7323
7353
 
7324
7354
  // src/i18n/review.json
@@ -7880,7 +7910,17 @@ var review_default = {
7880
7910
  "pause.on": "machine",
7881
7911
  "pause.report": "machine",
7882
7912
  "pause.no_session": "machine",
7883
- "pause.error.person_only": "machine"
7913
+ "pause.error.person_only": "machine",
7914
+ "secrets.incident.runbook": "machine",
7915
+ "secrets.command.live": "machine",
7916
+ "secrets.action.live": "machine",
7917
+ "secrets.live.dry": "machine",
7918
+ "secrets.live.contacting": "machine",
7919
+ "secrets.live.live": "machine",
7920
+ "secrets.live.rejected": "machine",
7921
+ "secrets.live.unknown": "machine",
7922
+ "secrets.live.unreachable": "machine",
7923
+ "secrets.live.no_value": "machine"
7884
7924
  },
7885
7925
  es: {
7886
7926
  "command.init.summary": "machine",
@@ -8439,7 +8479,17 @@ var review_default = {
8439
8479
  "pause.on": "machine",
8440
8480
  "pause.report": "machine",
8441
8481
  "pause.no_session": "machine",
8442
- "pause.error.person_only": "machine"
8482
+ "pause.error.person_only": "machine",
8483
+ "secrets.incident.runbook": "machine",
8484
+ "secrets.command.live": "machine",
8485
+ "secrets.action.live": "machine",
8486
+ "secrets.live.dry": "machine",
8487
+ "secrets.live.contacting": "machine",
8488
+ "secrets.live.live": "machine",
8489
+ "secrets.live.rejected": "machine",
8490
+ "secrets.live.unknown": "machine",
8491
+ "secrets.live.unreachable": "machine",
8492
+ "secrets.live.no_value": "machine"
8443
8493
  }
8444
8494
  };
8445
8495
 
@@ -8736,8 +8786,8 @@ var callersRate = (runtime, paths) => {
8736
8786
  const parsed = line2.trim() === "" ? void 0 : parseJsonObject(line2);
8737
8787
  if (parsed === void 0 || !parsed.ok || !isRecord(parsed.value)) continue;
8738
8788
  const event = parsed.value["event"];
8739
- const target = event === FLAGGED_EVENT ? flagged : event === RELEASED_EVENT ? released2 : void 0;
8740
- if (target !== void 0) for (const pair of pairs(parsed.value)) target.add(pair);
8789
+ const target2 = event === FLAGGED_EVENT ? flagged : event === RELEASED_EVENT ? released2 : void 0;
8790
+ if (target2 !== void 0) for (const pair of pairs(parsed.value)) target2.add(pair);
8741
8791
  }
8742
8792
  const total = (/* @__PURE__ */ new Set([...flagged, ...released2])).size;
8743
8793
  const rate = total === 0 ? 0 : released2.size / total;
@@ -8759,13 +8809,13 @@ var JS_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
8759
8809
  var stripModule = (path) => path.replace(JS_EXTENSION, "").replace(/\/index$/, "");
8760
8810
  var jsSpecifiers = (text6) => [...text6.matchAll(JS_SPECIFIER)].map((match) => match[1] ?? "");
8761
8811
  var jsImports = (callerPath, callerText, origin) => {
8762
- const target = stripModule(origin);
8812
+ const target2 = stripModule(origin);
8763
8813
  return jsSpecifiers(callerText).some((specifier) => {
8764
8814
  if (specifier.startsWith(".")) {
8765
- return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target;
8815
+ return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target2;
8766
8816
  }
8767
8817
  const bare = stripModule(specifier.replace(/^[@~#]\//, ""));
8768
- return target === bare || target.endsWith(`/${bare}`);
8818
+ return target2 === bare || target2.endsWith(`/${bare}`);
8769
8819
  });
8770
8820
  };
8771
8821
  var pyImports = (callerText, origin) => {
@@ -10283,13 +10333,13 @@ var MAX_FILE_BYTES = 2 * 1024 * 1024;
10283
10333
  var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
10284
10334
  var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
10285
10335
  var binary = (text6) => text6.slice(0, 8e3).includes("\0");
10286
- var listed2 = (runtime, root, target) => {
10287
- const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target], root);
10336
+ var listed2 = (runtime, root, target2) => {
10337
+ const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target2], root);
10288
10338
  if (git.ok)
10289
10339
  return accept(
10290
10340
  git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
10291
10341
  );
10292
- const base = toAbsolutePath(target, root);
10342
+ const base = toAbsolutePath(target2, root);
10293
10343
  const walked = runtime.walkFiles(base);
10294
10344
  if (!walked.ok) return walked;
10295
10345
  return accept(walked.value.map((name) => toPosixPath(relative4(root, join20(base, toPosixPath(name))))));
@@ -10299,8 +10349,8 @@ var scanFile = (runtime, root, file) => {
10299
10349
  if (!text6.ok || text6.value.length > MAX_FILE_BYTES || binary(text6.value)) return [];
10300
10350
  return scanText(text6.value, { path: file }).map((finding3) => locate(finding3, file, finding3.line));
10301
10351
  };
10302
- var scanPath = (runtime, root, target) => {
10303
- const files = listed2(runtime, root, target);
10352
+ var scanPath = (runtime, root, target2) => {
10353
+ const files = listed2(runtime, root, target2);
10304
10354
  if (!files.ok) return files;
10305
10355
  return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
10306
10356
  };
@@ -11320,7 +11370,7 @@ var integrationRefs = (runtime, root, defaultBranch) => [`refs/heads/${defaultBr
11320
11370
  var contained = (runtime, root, branch, targets2) => {
11321
11371
  const tip = line(runtime, root, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
11322
11372
  if (tip === void 0) return true;
11323
- return targets2.filter((target) => target !== `refs/heads/${branch}`).some((target) => runtime.git(["merge-base", "--is-ancestor", tip, target], root).ok);
11373
+ return targets2.filter((target2) => target2 !== `refs/heads/${branch}`).some((target2) => runtime.git(["merge-base", "--is-ancestor", tip, target2], root).ok);
11324
11374
  };
11325
11375
  var shared = (runtime, root, upstream, base) => {
11326
11376
  if (upstream === void 0 || !refExists(runtime, root, upstream.remoteRef)) return false;
@@ -12069,7 +12119,7 @@ var loadProdTargets = (runtime, root, config) => {
12069
12119
  var isProdContext = (name, declared) => declared.includes(name) || /(?:^|[-_.:/])(?:prod|production)(?:$|[-_.:/])/i.test(name);
12070
12120
 
12071
12121
  // src/prod/classify.ts
12072
- var hit2 = (effect, target, tool) => ({ effect, target, tool });
12122
+ var hit2 = (effect, target2, tool) => ({ effect, target: target2, tool });
12073
12123
  var sshHit = (command, isProd) => {
12074
12124
  const call = parseSsh(command);
12075
12125
  if (call === void 0 || !isProd(call.destination)) return void 0;
@@ -12105,9 +12155,9 @@ var dockerHit = (command, targets2, isProd) => {
12105
12155
  return hit2("effect", switching, "docker context");
12106
12156
  const prodHost = remote !== void 0 && isProd(remote) ? hostKey(remote) : void 0;
12107
12157
  const prodContext = context !== void 0 && isProdContext(context, targets2.dockerContexts) ? context : void 0;
12108
- const target = prodHost ?? prodContext;
12109
- if (target === void 0) return void 0;
12110
- return hit2(dockerReadOnly(command.args) ? "read" : "effect", target, "docker");
12158
+ const target2 = prodHost ?? prodContext;
12159
+ if (target2 === void 0) return void 0;
12160
+ return hit2(dockerReadOnly(command.args) ? "read" : "effect", target2, "docker");
12111
12161
  };
12112
12162
  var kubeHit = (command, targets2) => {
12113
12163
  const context = valueAfter(command.args, "--context", "--kube-context");
@@ -12792,8 +12842,8 @@ var pushDestinations = (parsed, facts) => {
12792
12842
  return [facts.upstream(current2)?.branch ?? current2];
12793
12843
  }
12794
12844
  return refspecs.flatMap((refspec) => {
12795
- const target = refspecTarget(refspec, current2);
12796
- return target === void 0 ? [] : [target];
12845
+ const target2 = refspecTarget(refspec, current2);
12846
+ return target2 === void 0 ? [] : [target2];
12797
12847
  });
12798
12848
  };
12799
12849
  var leaseVerdict = (parsed, facts) => {
@@ -12973,8 +13023,8 @@ var guardGitCommand = (context, command, depth = 0) => {
12973
13023
  if (repo === void 0) return [];
12974
13024
  const facts = withChain(context.facts?.(repo) ?? repoFacts(context.runtime, repo), context.chain);
12975
13025
  const expanded = expandAlias(context.runtime, repo, invocation);
12976
- const target = switchTarget(expanded, facts);
12977
- if (target !== void 0 && context.chain !== void 0) context.chain.branch = target;
13026
+ const target2 = switchTarget(expanded, facts);
13027
+ if (target2 !== void 0 && context.chain !== void 0) context.chain.branch = target2;
12978
13028
  const nested = depth < MAX_ALIAS_DEPTH ? shellAliasCommands(expanded, context.runtime, repo).flatMap(
12979
13029
  (inner) => guardGitCommand(context, inner, depth + 1)
12980
13030
  ) : [];
@@ -13011,11 +13061,11 @@ var removalTargets = (command) => {
13011
13061
  if (command.program === "remove-item") return powershellTargets(command);
13012
13062
  return void 0;
13013
13063
  };
13014
- var globBase = (target) => {
13015
- const at = target.search(GLOB2);
13016
- if (at < 0) return target;
13017
- const slash = target.lastIndexOf("/", at);
13018
- return slash < 0 ? "." : target.slice(0, slash) || "/";
13064
+ var globBase = (target2) => {
13065
+ const at = target2.search(GLOB2);
13066
+ if (at < 0) return target2;
13067
+ const slash = target2.lastIndexOf("/", at);
13068
+ return slash < 0 ? "." : target2.slice(0, slash) || "/";
13019
13069
  };
13020
13070
  var containsGitDir = (runtime, path) => runtime.stat(toAbsolutePath(resolveFrom(path, ".git"))) !== void 0;
13021
13071
  var targetVerdict = (runtime, context, command, raw) => {
@@ -13035,8 +13085,8 @@ var targetVerdict = (runtime, context, command, raw) => {
13035
13085
  var rmVerdict = (runtime, context, command) => {
13036
13086
  const targets2 = removalTargets(command);
13037
13087
  if (targets2 === void 0) return PASS;
13038
- for (const target of targets2) {
13039
- const verdict = targetVerdict(runtime, context, command, target);
13088
+ for (const target2 of targets2) {
13089
+ const verdict = targetVerdict(runtime, context, command, target2);
13040
13090
  if (verdict.kind === "block") return verdict;
13041
13091
  }
13042
13092
  return PASS;
@@ -13099,7 +13149,7 @@ var appendJournal = (runtime, root, entry2) => {
13099
13149
 
13100
13150
  // src/git/snapshot.ts
13101
13151
  var BACKUP_PREFIX = "refs/finitemonkey/backup/";
13102
- var DEFAULT_SNAPSHOT_TIMEOUT_MS = 2500;
13152
+ var DEFAULT_SNAPSHOT_TIMEOUT_MS = 5e3;
13103
13153
  var stampOf = (iso) => iso.replace(/[:.]/g, "-");
13104
13154
  var slug = (op) => op.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "manual";
13105
13155
  var optionalLine = (runtime, repo, args2) => {
@@ -13574,12 +13624,12 @@ var sourceOf = (value, isFile, segment, heredocs) => {
13574
13624
  const piped = segment.args.map((arg) => heredocIn(arg, heredocs)).find((text6) => text6 !== void 0);
13575
13625
  return { kind: "inline", text: piped ?? "" };
13576
13626
  };
13577
- var collect3 = (target, segment, args2, spec2, heredocs) => {
13627
+ var collect3 = (target2, segment, args2, spec2, heredocs) => {
13578
13628
  const values = flagValues(args2, spec2);
13579
13629
  if (values.length === 0) return void 0;
13580
13630
  const title = values.find((item) => spec2.title.includes(item.flag));
13581
13631
  const bodies = values.filter((item) => !spec2.title.includes(item.flag)).map((item) => sourceOf(item.value, spec2.file.includes(item.flag), segment, heredocs));
13582
- return { target, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
13632
+ return { target: target2, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
13583
13633
  };
13584
13634
  var gitSubcommand = (args2) => {
13585
13635
  let index = 0;
@@ -13798,7 +13848,7 @@ var READERS = /* @__PURE__ */ new Set([
13798
13848
  "dotenv"
13799
13849
  ]);
13800
13850
  var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
13801
- var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
13851
+ var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b|live\b[^;&|\n]*\s--send\b)/;
13802
13852
  var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
13803
13853
  var onlyFlags = (args2) => args2.every((arg) => arg.startsWith("-"));
13804
13854
  var readTarget = (segment) => {
@@ -13824,8 +13874,8 @@ var bashFindings = (command, segments) => {
13824
13874
  const person = PERSON_ONLY.exec(command);
13825
13875
  if (person !== null) return [{ kind: "person-only", command: person[0] }];
13826
13876
  return segments.flatMap((segment) => {
13827
- const target = readTarget(segment);
13828
- if (target !== void 0) return [{ kind: "read", target }];
13877
+ const target2 = readTarget(segment);
13878
+ if (target2 !== void 0) return [{ kind: "read", target: target2 }];
13829
13879
  if (dumpsEnv(segment)) return [{ kind: "env-dump", command: [segment.program, ...segment.args].join(" ") }];
13830
13880
  return [];
13831
13881
  });
@@ -13916,9 +13966,9 @@ var bashDecision = (input) => {
13916
13966
  };
13917
13967
  var readDecision = (input) => {
13918
13968
  const inputs = toolInput2(input.payload);
13919
- const target = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
13920
- if (target === "" || !isSensitivePath(target)) return ALLOW5;
13921
- return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target) }) };
13969
+ const target2 = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
13970
+ if (target2 === "" || !isSensitivePath(target2)) return ALLOW5;
13971
+ return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target2) }) };
13922
13972
  };
13923
13973
  var WRITERS2 = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
13924
13974
  var SHELLS4 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
@@ -16298,15 +16348,15 @@ var problem2 = (code, messageKey, params2) => ({
16298
16348
  exitCode: ExitCode.Usage
16299
16349
  });
16300
16350
  var underRoot = (root, path) => toPosixPath(relative8(root, path));
16301
- var expand2 = (ctx, root, target) => {
16302
- const path = toAbsolutePath(target, ctx.cwd);
16351
+ var expand2 = (ctx, root, target2) => {
16352
+ const path = toAbsolutePath(target2, ctx.cwd);
16303
16353
  const stat = ctx.runtime.stat(path);
16304
16354
  if (stat === void 0)
16305
- return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target, detail: "not found" }));
16355
+ return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target2, detail: "not found" }));
16306
16356
  if (stat.kind === "file") return accept([underRoot(root, path)]);
16307
16357
  const walked = ctx.runtime.walkFiles(path);
16308
16358
  if (!walked.ok)
16309
- return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target, detail: walked.error }));
16359
+ return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target2, detail: walked.error }));
16310
16360
  return accept(
16311
16361
  walked.value.map((name) => underRoot(root, toAbsolutePath(toPosixPath(name), path))).filter((name) => TEXT_FILE.test(name) && !/(?:^|\/)(?:node_modules|\.git)\//.test(name)).sort()
16312
16362
  );
@@ -16490,6 +16540,33 @@ var secretsAcceptCommand = {
16490
16540
  }
16491
16541
  };
16492
16542
 
16543
+ // src/secrets/runbooks.ts
16544
+ import { dirname as dirname9, join as join39 } from "node:path";
16545
+ var PROVIDER_OF_RULE = {
16546
+ "aws-access-token": "aws",
16547
+ "aws-secret-access-key": "aws",
16548
+ "github-pat": "github",
16549
+ "github-oauth": "github",
16550
+ "github-app-token": "github",
16551
+ "github-refresh-token": "github",
16552
+ "github-fine-grained-pat": "github",
16553
+ "stripe-access-token": "stripe",
16554
+ "anthropic-api-key": "anthropic",
16555
+ "openai-api-key": "openai",
16556
+ "gcp-api-key": "google",
16557
+ "asaas-api-key": "asaas",
16558
+ "credential-in-url": "database",
16559
+ "password-assignment": "database",
16560
+ "secret-in-process-args": "database"
16561
+ };
16562
+ var RUNBOOKS_URL = "https://github.com/gabrielvieiraf/finitemonkey/blob/main/runbooks";
16563
+ var runbookProvider = (rule) => PROVIDER_OF_RULE[rule] ?? "generic";
16564
+ var runbookLink = (runtime, rule) => {
16565
+ const file = `${runbookProvider(rule)}.md`;
16566
+ const local = toAbsolutePath(join39(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
16567
+ return runtime.stat(local)?.kind === "file" ? local : `${RUNBOOKS_URL}/${file}`;
16568
+ };
16569
+
16493
16570
  // src/commands/secrets-incident.ts
16494
16571
  var INCIDENT_ACTIONS = ["list", "rotated", "close", "purge"];
16495
16572
  var USAGE = "finitemonkey secrets incident [list | rotated <id> | close <id> | purge <id>]";
@@ -16515,9 +16592,19 @@ var itemLine = (ctx, incident) => ctx.t("secrets.incident.item", {
16515
16592
  where: incident.where,
16516
16593
  created: incident.createdAt.slice(0, 10)
16517
16594
  });
16595
+ var itemLines2 = (ctx, incident) => [
16596
+ itemLine(ctx, incident),
16597
+ ctx.t("secrets.incident.runbook", { link: incident.runbook })
16598
+ ];
16518
16599
  var list2 = (ctx, root) => {
16519
- const incidents = listIncidents(ctx.runtime, root);
16520
- const human2 = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [ctx.t("secrets.incident.title", { count: incidents.length }), ...incidents.map((item) => itemLine(ctx, item))];
16600
+ const incidents = listIncidents(ctx.runtime, root).map((incident) => ({
16601
+ ...incident,
16602
+ runbook: runbookLink(ctx.runtime, incident.rule)
16603
+ }));
16604
+ const human2 = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [
16605
+ ctx.t("secrets.incident.title", { count: incidents.length }),
16606
+ ...incidents.flatMap((item) => itemLines2(ctx, item))
16607
+ ];
16521
16608
  return success({ schema: "finitemonkey/secrets-incidents@1", incidents }, human2);
16522
16609
  };
16523
16610
  var notRotated = (ctx, incident) => fail2(ctx, {
@@ -16584,6 +16671,133 @@ var secretsIncidentCommand = {
16584
16671
  }
16585
16672
  };
16586
16673
 
16674
+ // src/secrets/live.ts
16675
+ var bearer = (key) => [`Authorization: Bearer ${key}`];
16676
+ var LIVE_PROBES = {
16677
+ github: {
16678
+ host: "api.github.com",
16679
+ path: "/user",
16680
+ headers: (key) => [...bearer(key), "User-Agent: finitemonkey"]
16681
+ },
16682
+ stripe: { host: "api.stripe.com", path: "/v1/balance", headers: bearer },
16683
+ openai: { host: "api.openai.com", path: "/v1/models", headers: bearer },
16684
+ anthropic: {
16685
+ host: "api.anthropic.com",
16686
+ path: "/v1/models",
16687
+ headers: (key) => [`x-api-key: ${key}`, "anthropic-version: 2023-06-01"]
16688
+ },
16689
+ digitalocean: { host: "api.digitalocean.com", path: "/v2/account", headers: bearer },
16690
+ cloudflare: { host: "api.cloudflare.com", path: "/client/v4/user/tokens/verify", headers: bearer },
16691
+ asaas: {
16692
+ host: "api.asaas.com",
16693
+ path: "/v3/finance/balance",
16694
+ headers: (key) => [`access_token: ${key}`, "User-Agent: finitemonkey"]
16695
+ }
16696
+ };
16697
+ var LIVE_PROVIDERS = Object.keys(LIVE_PROBES).filter((name) => name in LIVE_PROBES);
16698
+ var LIVE_HOSTS = LIVE_PROVIDERS.map((name) => LIVE_PROBES[name].host);
16699
+ var liveProvider = (name) => LIVE_PROVIDERS.find((provider) => provider === name);
16700
+ var LIVE_TIMEOUT_MS = 15e3;
16701
+ var liveRequest = (provider, key, cwd) => {
16702
+ const probe = LIVE_PROBES[provider];
16703
+ return {
16704
+ command: "curl",
16705
+ args: [
16706
+ "--silent",
16707
+ "--show-error",
16708
+ "--proto",
16709
+ "=https",
16710
+ "--max-time",
16711
+ "10",
16712
+ "--header",
16713
+ "@-",
16714
+ "--write-out",
16715
+ "\n%{http_code}",
16716
+ `https://${probe.host}${probe.path}`
16717
+ ],
16718
+ cwd,
16719
+ timeoutMs: LIVE_TIMEOUT_MS,
16720
+ input: `${probe.headers(key).join("\n")}
16721
+ `
16722
+ };
16723
+ };
16724
+ var liveOutcome = (result) => {
16725
+ if (!result.found || result.exitCode !== 0) return { verdict: "unreachable" };
16726
+ const status = Number.parseInt(result.stdout.trimEnd().split("\n").at(-1) ?? "", 10);
16727
+ if (!Number.isInteger(status) || status === 0) return { verdict: "unreachable" };
16728
+ if (status >= 200 && status < 300) return { verdict: "live", status };
16729
+ if (status === 401 || status === 403) return { verdict: "rejected", status };
16730
+ return { verdict: "unknown", status };
16731
+ };
16732
+
16733
+ // src/commands/secrets-live.ts
16734
+ var SECRETS_LIVE_SCHEMA = "finitemonkey/secrets-live@1";
16735
+ var USAGE2 = `finitemonkey secrets live <${LIVE_PROVIDERS.join("|")}> --env <VARIABLE> [--send]`;
16736
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16737
+ var target = (provider) => {
16738
+ const probe = LIVE_PROBES[provider];
16739
+ return { host: probe.host, url: `https://${probe.host}${probe.path}` };
16740
+ };
16741
+ var dryRun = (ctx, provider, variable) => {
16742
+ const { host, url: url2 } = target(provider);
16743
+ const data = { schema: SECRETS_LIVE_SCHEMA, provider, host, url: url2, variable, sent: false };
16744
+ return success(data, [ctx.t("secrets.live.dry", { host, url: url2, variable, provider })]);
16745
+ };
16746
+ var send = (ctx, provider, variable, key) => {
16747
+ const { host, url: url2 } = target(provider);
16748
+ ctx.io.stderr.write(`${ctx.t("secrets.live.contacting", { host, url: url2 })}
16749
+ `);
16750
+ const outcome2 = liveOutcome(ctx.runtime.exec(liveRequest(provider, key, ctx.cwd)));
16751
+ const data = { schema: SECRETS_LIVE_SCHEMA, provider, host, url: url2, variable, sent: true, ...outcome2 };
16752
+ const line2 = ctx.t(`secrets.live.${outcome2.verdict}`, { host, status: outcome2.status ?? 0, variable });
16753
+ if (outcome2.verdict !== "unreachable") return success(data, [line2]);
16754
+ return failureFrom(ctx.t, {
16755
+ code: "secrets_live_unreachable",
16756
+ messageKey: "secrets.live.unreachable",
16757
+ params: { host, status: 0, variable },
16758
+ fix: "install curl or check the network, then run the command again",
16759
+ exitCode: ExitCode.CheckFailed,
16760
+ data
16761
+ });
16762
+ };
16763
+ var secretsLiveCommand = {
16764
+ path: ["secrets", "live"],
16765
+ summaryKey: "secrets.command.live",
16766
+ usage: USAGE2,
16767
+ status: "ready",
16768
+ maxPositionals: 1,
16769
+ options: { env: { type: "string" }, send: { type: "boolean" } },
16770
+ run: (ctx, args2) => {
16771
+ const provider = liveProvider(args2.positionals[0]);
16772
+ const variable = stringOption(args2.values, "env") ?? "";
16773
+ if (provider === void 0 || !ENV_NAME.test(variable))
16774
+ return Promise.resolve(
16775
+ failureFrom(
16776
+ ctx.t,
16777
+ usageProblem("invalid_arguments", "error.invalid_arguments", USAGE2, {
16778
+ detail: provider === void 0 ? args2.positionals[0] ?? "<provider>" : "--env <VARIABLE>"
16779
+ })
16780
+ )
16781
+ );
16782
+ if (args2.values["send"] !== true) return Promise.resolve(dryRun(ctx, provider, variable));
16783
+ if (!isPerson(ctx))
16784
+ return Promise.resolve(
16785
+ failureFrom(
16786
+ ctx.t,
16787
+ needsPersonProblem("secrets.error.person_only", `${USAGE2.replace(" [--send]", "")} --send`, {
16788
+ action: ctx.t("secrets.action.live")
16789
+ })
16790
+ )
16791
+ );
16792
+ const key = ctx.env[variable] ?? "";
16793
+ if (key === "")
16794
+ return Promise.resolve(
16795
+ failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.live.no_value", USAGE2, { variable }))
16796
+ );
16797
+ return Promise.resolve(send(ctx, provider, variable, key));
16798
+ }
16799
+ };
16800
+
16587
16801
  // src/commands/secrets-scan.ts
16588
16802
  var SECRETS_SCAN_SCHEMA = "finitemonkey/secrets-scan@1";
16589
16803
  var view = (finding3, status) => ({
@@ -16604,15 +16818,15 @@ var modeOf3 = (values) => {
16604
16818
  if (chosen[0] === true) return "staged";
16605
16819
  return chosen[1] === true ? "history" : "path";
16606
16820
  };
16607
- var normalizedTarget = (target) => target.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
16608
- var scopeOf = (target) => {
16609
- const prefix = normalizedTarget(target);
16821
+ var normalizedTarget = (target2) => target2.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
16822
+ var scopeOf = (target2) => {
16823
+ const prefix = normalizedTarget(target2);
16610
16824
  return (file) => prefix === "" || file === prefix || file.startsWith(`${prefix}/`);
16611
16825
  };
16612
- var runScan = async (ctx, root, mode, target) => {
16826
+ var runScan = async (ctx, root, mode, target2) => {
16613
16827
  if (mode === "staged") return scanStaged(ctx.runtime, root);
16614
16828
  if (mode === "history") return scanHistory(ctx.runtime, root);
16615
- return scanPath(ctx.runtime, root, normalizedTarget(target) || ".");
16829
+ return scanPath(ctx.runtime, root, normalizedTarget(target2) || ".");
16616
16830
  };
16617
16831
  var historyIncidents = (ctx, root, fresh3) => fresh3.flatMap((finding3) => {
16618
16832
  if (ruleById(finding3.ruleId)?.kind !== "credential") return [];
@@ -16693,8 +16907,8 @@ var secretsScanCommand = {
16693
16907
  const fix = "finitemonkey help secrets scan";
16694
16908
  if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
16695
16909
  const root = projectRoot(ctx);
16696
- const target = stringOption(args2.values, "path") ?? ".";
16697
- const scanned = await runScan(ctx, root, mode, target);
16910
+ const target2 = stringOption(args2.values, "path") ?? ".";
16911
+ const scanned = await runScan(ctx, root, mode, target2);
16698
16912
  if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
16699
16913
  const baseline = loadBaseline(ctx.runtime, root);
16700
16914
  if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
@@ -16702,7 +16916,7 @@ var secretsScanCommand = {
16702
16916
  scanned.value,
16703
16917
  baseline.value,
16704
16918
  ctx.runtime.now(),
16705
- mode === "path" ? scopeOf(target) : void 0
16919
+ mode === "path" ? scopeOf(target2) : void 0
16706
16920
  );
16707
16921
  if (mode === "path") prune(ctx, root, baseline.value, outcome2);
16708
16922
  const incidents = mode === "history" ? historyIncidents(ctx, root, outcome2.fresh) : [];
@@ -16711,7 +16925,7 @@ var secretsScanCommand = {
16711
16925
  };
16712
16926
 
16713
16927
  // src/task/create.ts
16714
- import { join as join39 } from "node:path";
16928
+ import { join as join40 } from "node:path";
16715
16929
 
16716
16930
  // src/core/yaml/write.ts
16717
16931
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -16800,10 +17014,10 @@ var explicitChecks = (draft) => [
16800
17014
  var draftChecks = (runtime, paths, draft) => {
16801
17015
  const disk = {
16802
17016
  read: (relative9) => {
16803
- const text6 = runtime.readText(toAbsolutePath(join39(paths.root, relative9)));
17017
+ const text6 = runtime.readText(toAbsolutePath(join40(paths.root, relative9)));
16804
17018
  return text6.ok ? text6.value : void 0;
16805
17019
  },
16806
- exists: (relative9) => runtime.stat(toAbsolutePath(join39(paths.root, relative9))) !== void 0
17020
+ exists: (relative9) => runtime.stat(toAbsolutePath(join40(paths.root, relative9))) !== void 0
16807
17021
  };
16808
17022
  const detected = detectChecks(disk);
16809
17023
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -16933,8 +17147,8 @@ var showTask = (ctx, id) => {
16933
17147
  const paths = projectOf(ctx);
16934
17148
  if (paths === void 0) return noRepo(ctx);
16935
17149
  const active = readState(ctx.runtime, paths).active;
16936
- const target = id ?? active;
16937
- const report2 = currentReport(ctx.runtime, paths, void 0, target);
17150
+ const target2 = id ?? active;
17151
+ const report2 = currentReport(ctx.runtime, paths, void 0, target2);
16938
17152
  if (report2 === void 0) {
16939
17153
  return failureFrom(
16940
17154
  ctx.t,
@@ -16942,7 +17156,7 @@ var showTask = (ctx, id) => {
16942
17156
  "task_not_found",
16943
17157
  "error.task_not_found",
16944
17158
  "finitemonkey task new <id> --goal <text>",
16945
- { task: target ?? "-" },
17159
+ { task: target2 ?? "-" },
16946
17160
  ExitCode.NotConfigured
16947
17161
  )
16948
17162
  );
@@ -16952,7 +17166,7 @@ var showTask = (ctx, id) => {
16952
17166
  var doneTask = (ctx, id) => {
16953
17167
  const paths = projectOf(ctx);
16954
17168
  if (paths === void 0) return noRepo(ctx);
16955
- const target = id ?? readState(ctx.runtime, paths).active;
17169
+ const target2 = id ?? readState(ctx.runtime, paths).active;
16956
17170
  const run4 = runGate({
16957
17171
  runtime: ctx.runtime,
16958
17172
  cwd: ctx.cwd,
@@ -16961,7 +17175,7 @@ var doneTask = (ctx, id) => {
16961
17175
  budgetMs: DEFAULT_GATE_BUDGET_MS,
16962
17176
  fast: false,
16963
17177
  noSkip: true,
16964
- ...target === void 0 ? {} : { taskId: target }
17178
+ ...target2 === void 0 ? {} : { taskId: target2 }
16965
17179
  });
16966
17180
  const result = runResult(ctx, run4, true);
16967
17181
  if (!result.ok || run4.kind !== "done" || run4.report.implicit) return result;
@@ -17053,9 +17267,9 @@ var isEmptyDir = (project, relative9) => {
17053
17267
  return listed4.ok && listed4.value.length === 0;
17054
17268
  };
17055
17269
  var finish3 = (project, plan) => {
17056
- const target = plan.purge ? STATE_DIR : LOCK_FILE;
17057
- const removed = project.runtime.remove(project.abs(target));
17058
- if (!removed.ok) return reject({ path: target, detail: removed.error });
17270
+ const target2 = plan.purge ? STATE_DIR : LOCK_FILE;
17271
+ const removed = project.runtime.remove(project.abs(target2));
17272
+ if (!removed.ok) return reject({ path: target2, detail: removed.error });
17059
17273
  for (const dir of plan.removeDirs) if (isEmptyDir(project, dir)) project.runtime.remove(project.abs(dir));
17060
17274
  return accept(void 0);
17061
17275
  };
@@ -17201,6 +17415,7 @@ var REGISTRY = [
17201
17415
  offCommand,
17202
17416
  onCommand,
17203
17417
  secretsIncidentCommand,
17418
+ secretsLiveCommand,
17204
17419
  proseCheckCommand,
17205
17420
  snapshotCommand,
17206
17421
  undoCommand,
@@ -17212,7 +17427,7 @@ var REGISTRY = [
17212
17427
  ];
17213
17428
 
17214
17429
  // src/i18n/lang-settings.ts
17215
- import { join as join40 } from "node:path";
17430
+ import { join as join41 } from "node:path";
17216
17431
  var read3 = (runtime, path) => {
17217
17432
  const text6 = runtime.readText(toAbsolutePath(path));
17218
17433
  return text6.ok ? text6.value : void 0;
@@ -17227,14 +17442,14 @@ var languageIn = (text6) => {
17227
17442
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
17228
17443
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
17229
17444
  };
17230
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join40(runtime.homeDir(), ".claude");
17445
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join41(runtime.homeDir(), ".claude");
17231
17446
  var langSettings = (runtime, cwd, env, platform) => {
17232
17447
  const root = findGitRoot(cwd, runtime) ?? cwd;
17233
- const config = parseConfigYaml(read3(runtime, join40(root, CONFIG_FILE)) ?? "");
17448
+ const config = parseConfigYaml(read3(runtime, join41(root, CONFIG_FILE)) ?? "");
17234
17449
  const claudeFiles = [
17235
- join40(root, ".claude", "settings.local.json"),
17236
- join40(root, ".claude", "settings.json"),
17237
- join40(claudeDir(runtime, env), "settings.json")
17450
+ join41(root, ".claude", "settings.local.json"),
17451
+ join41(root, ".claude", "settings.json"),
17452
+ join41(claudeDir(runtime, env), "settings.json")
17238
17453
  ];
17239
17454
  return {
17240
17455
  config: configText(config, "lang"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finitemonkey",
3
- "version": "0.1.0-next.18",
3
+ "version": "0.1.0-next.19",
4
4
  "description": "Claude Code hooks that stop a coding agent from saying done before the tests pass: a machine-checked definition of done, plus guardrails for secrets, git safety and production. Pre-release.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "files": [
35
35
  "dist",
36
+ "runbooks",
36
37
  "LICENSE",
37
38
  "NOTICE",
38
39
  "README.md"
@@ -55,7 +56,7 @@
55
56
  "i18n:check": "node dist/finitemonkey.mjs i18n check --dir src/i18n",
56
57
  "bench:startup": "node scripts/bench-startup.mjs",
57
58
  "smoke:install": "node scripts/smoke-install.mjs",
58
- "check": "npm run typecheck && npm run lint && npm run format:check && npm run check:dist && npm test && npm run i18n:check && npm run plugin:validate && npm pack --dry-run && npm run llms:check && npm run schema:check && npm run docs:check",
59
+ "check": "npm run typecheck && npm run lint && npm run format:check && npm run check:dist && npm test && npm run i18n:check && npm run plugin:validate && npm pack --dry-run && npm run llms:check && npm run schema:check && npm run docs:check && npm run runbooks:check",
59
60
  "prepublishOnly": "npm run typecheck && npm run check:dist && npm test",
60
61
  "prepare": "husky",
61
62
  "gif": "tsc -p assets/scripts && node --experimental-strip-types assets/scripts/render.ts",
@@ -68,6 +69,7 @@
68
69
  "budgets": "tsx scripts/budgets.ts",
69
70
  "docs": "tsx scripts/build-docs.ts",
70
71
  "docs:check": "tsc -p scripts && tsx scripts/build-docs.ts --check",
72
+ "runbooks:check": "tsx scripts/check-runbooks.ts",
71
73
  "e2e:claude": "node --import tsx --test --test-concurrency=1 test/e2e/claude-stop.e2e.ts",
72
74
  "plugin:sync": "node scripts/plugin-sync.mjs",
73
75
  "plugin:validate": "node scripts/plugin-validate.mjs",
@@ -0,0 +1,31 @@
1
+ # Anthropic API keys
2
+
3
+ Applies to rule `anthropic-api-key`.
4
+
5
+ ## 1. Revoke
6
+
7
+ In the Claude Console, open **Settings** > **API keys** and disable or delete the key. With an admin key, `POST /v1/organizations/api_keys/{api_key_id}` with `{"status": "inactive"}` does the same.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Create the new key in a workspace that holds only this application, with spend limits set on that workspace. Do not reuse one key across environments.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the secret store, CI secrets and every service that reads `ANTHROPIC_API_KEY`.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Review usage and cost for the workspace in the window. Requests or spend you cannot explain mean the key was used by someone else.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ After a few days with no errors from consumers, archive or delete the disabled key.
24
+
25
+ ## 6. Notification duties
26
+
27
+ Prompts and files sent with the key may hold personal data. If the key could read stored files or batches, assess notice duties.
28
+
29
+ ## Sources
30
+
31
+ - https://platform.claude.com/docs/en/api/admin-api/apikeys/update-api-key (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # Asaas API keys
2
+
3
+ Applies to rule `asaas-api-key`.
4
+
5
+ ## 1. Revoke
6
+
7
+ As an account administrator, open **Integrações** in the web interface and delete the exposed key. A deleted key cannot be restored. A parent account can also revoke a subaccount key through the API.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Create the new key in the same area. Use a separate key per subaccount and per environment, and never put a production key in a sandbox configuration.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the backend secret store, CI secrets and every worker that calls the API.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Review charges, transfers, PIX keys, bank account changes and webhook settings for the window, in the account and in every subaccount the key could reach.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ If you created a temporary key during the switch, delete it after a few days without errors.
24
+
25
+ ## 6. Notification duties
26
+
27
+ Asaas keys reach customer names, tax ids and payment data. If they were read, assess notice duties under the LGPD toward the ANPD and the people affected.
28
+
29
+ ## Sources
30
+
31
+ - https://docs.asaas.com/docs/chaves-de-api (accessed 2026-10-26)
@@ -0,0 +1,32 @@
1
+ # AWS access keys
2
+
3
+ Applies to rules `aws-access-token` and `aws-secret-access-key`.
4
+
5
+ ## 1. Revoke
6
+
7
+ In the IAM console, open the user, then **Security credentials** > **Access keys**, and set the exposed key to **Inactive** (`aws iam update-access-key --access-key-id <id> --status Inactive --user-name <user>`). Inactive keys stop signing requests at once and can still be turned back on if something critical breaks.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Prefer no long-lived key at all: an IAM role for EC2, ECS or Lambda, or OIDC federation for CI (GitHub Actions `aws-actions/configure-aws-credentials` with a role). If a key is unavoidable, create it on a user whose policy grants only what the consumer needs (`aws iam create-access-key --user-name <user>`).
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the secret store, CI secrets, `.env` files outside git, and every host that had the old key. Restart the processes that read it at start.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Run `aws iam get-access-key-last-used --access-key-id <id>` and search CloudTrail for the key id from the first exposure until it went inactive. Look for new IAM users, new keys, new regions, EC2 instances you did not start and S3 reads you do not recognize.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ After a few days with no errors from consumers and no use of the old id, delete it (`aws iam delete-access-key --access-key-id <id> --user-name <user>`).
24
+
25
+ ## 6. Notification duties
26
+
27
+ If CloudTrail shows access to personal data, the data protection law that applies (LGPD, GDPR or a state law) may require notice to the regulator and to the people affected. Record what was reachable with the key and decide with whoever owns that duty.
28
+
29
+ ## Sources
30
+
31
+ - https://docs.aws.amazon.com/IAM/latest/UserGuide/id-credentials-access-keys-update.html (accessed 2026-10-26)
32
+ - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # Cloudflare API tokens
2
+
3
+ Applies to Cloudflare API tokens and global API keys found by the generic rules.
4
+
5
+ ## 1. Revoke
6
+
7
+ Go to **My Profile** > **API Tokens**, open the menu next to the token and choose **Roll**. Rolling invalidates the old secret at once and keeps the same permissions. A global API key can be changed on the same page.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Prefer a scoped API token over the global API key: only the zones and permissions the consumer needs, with client IP filtering and a TTL when the consumer runs from known addresses.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update CI secrets, Workers secrets, Terraform variables and any tool that calls the API.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Read the account audit log for the window: DNS changes, new Workers or routes, page rules, firewall changes, new members and new tokens.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ Delete any extra token created during the change once consumers have run without errors for a few days.
24
+
25
+ ## 6. Notification duties
26
+
27
+ DNS or Workers changes can redirect user traffic. If that happened, treat it as a breach of the affected service and assess notice duties.
28
+
29
+ ## Sources
30
+
31
+ - https://developers.cloudflare.com/fundamentals/api/how-to/roll-token/ (accessed 2026-10-26)
@@ -0,0 +1,32 @@
1
+ # Database and Redis passwords
2
+
3
+ Applies to rules `credential-in-url`, `password-assignment` and `secret-in-process-args` when the value is a database or Redis password.
4
+
5
+ ## 1. Revoke
6
+
7
+ PostgreSQL: `ALTER ROLE <role> WITH PASSWORD NULL;` or `ALTER ROLE <role> NOLOGIN;` until the new password is in place. MySQL: `ALTER USER '<user>'@'<host>' ACCOUNT LOCK;`. Redis with ACLs: remove the password from the user (`ACL SETUSER <user> <<old-password>`), or with `requirepass` change it in the config and restart.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Generate a long random password (`ACL GENPASS` in Redis, `openssl rand -base64 32` elsewhere), or move to IAM database authentication or short-lived credentials from a secrets manager. Give each application its own role with only the grants it needs.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update connection strings in the secret store, compose files (from a variable, never a literal argument), CI, migrations and backup jobs. Restart connection pools.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Check the server log for logins from unknown addresses, `pg_stat_activity` or `SHOW PROCESSLIST` for unknown sessions, and look for new roles, changed grants, dropped tables or large reads. Check whether the port was reachable from the internet during the window.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ Remove temporary roles or extra Redis passwords added for the switch after the applications have run cleanly for a few days.
24
+
25
+ ## 6. Notification duties
26
+
27
+ A database usually holds personal data. If anyone outside your team could have connected, treat it as a possible breach and assess notice duties toward the regulator and the people affected.
28
+
29
+ ## Sources
30
+
31
+ - https://www.postgresql.org/docs/17/sql-alterrole.html (accessed 2026-10-26)
32
+ - https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/ (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # DigitalOcean API tokens
2
+
3
+ Applies to DigitalOcean personal access tokens (`dop_v1_`) found by the generic rules.
4
+
5
+ ## 1. Revoke
6
+
7
+ In the control panel, open **API** > **Tokens**, find the token and choose **Delete**, or **Regenerate** if consumers must keep the same name and scopes.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Create a token with custom scopes limited to the resources the consumer touches and an expiration date. Scopes cannot be edited later, so pick them now.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update CI secrets, `doctl auth` contexts, Terraform variables and every host that used the old token.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Review the account's security history and the resource list for the window: new Droplets, volumes, snapshots, firewall changes, new SSH keys and Spaces access keys you did not create.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ If you regenerated instead of deleting, confirm nothing still fails with the old value, then delete any leftover token created for the transition.
24
+
25
+ ## 6. Notification duties
26
+
27
+ If Droplets or databases with personal data were reachable, assess notice duties under the law that applies to that data.
28
+
29
+ ## Sources
30
+
31
+ - https://docs.digitalocean.com/reference/api/create-personal-access-token/ (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # Any other credential
2
+
3
+ Applies to rules without a provider runbook: `generic-api-key`, `jwt`, `private-key`, `gitlab-pat`, `npm-access-token` and the Slack rules.
4
+
5
+ ## 1. Revoke
6
+
7
+ Find the provider that issued the value and revoke or disable it there. For a private key, remove its public half from every place that trusts it (`authorized_keys`, deploy keys, certificates). For a JWT signing secret, rotate the secret, which invalidates every token it signed.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Prefer short-lived credentials (OIDC, roles, tokens with an expiry) over long-lived ones, and give the new value only the scope the consumer needs.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the secret store, CI secrets, `.env` files outside git and every host that had the old value.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Read the provider's audit or access log from the first exposure until revocation and look for actions you did not take.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ Delete leftover records of the old value once consumers have run without errors for a few days.
24
+
25
+ ## 6. Notification duties
26
+
27
+ If the credential reached personal data, assess notice duties under the law that applies.
28
+
29
+ ## Sources
30
+
31
+ - https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html (accessed 2026-10-26)
@@ -0,0 +1,32 @@
1
+ # GitHub tokens
2
+
3
+ Applies to rules `github-pat`, `github-oauth`, `github-app-token`, `github-refresh-token` and `github-fine-grained-pat`.
4
+
5
+ ## 1. Revoke
6
+
7
+ For a personal access token, go to **Settings** > **Developer settings** > **Personal access tokens** and delete it. For an OAuth or GitHub App token, revoke the authorization under **Settings** > **Applications**, or rotate the app's client secret. A revoked token cannot be restored.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Use the narrowest path that works: `GITHUB_TOKEN` inside Actions, a GitHub App installation token for automation, or a fine-grained personal access token limited to the repositories and permissions the consumer needs, with an expiration date.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update repository and organization secrets, CI variables, local `gh auth` sessions, git remotes that carried the token in the URL (`git remote set-url`) and credential helpers.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Check the security log (**Settings** > **Security log**, or the organization audit log) for the window: new deploy keys, new tokens, changed branch protection, pushes and releases you did not make, and repositories made public or transferred.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ A deleted token is already gone. If you only reduced an app's access, remove the old installation or secret after a few days without failures.
24
+
25
+ ## 6. Notification duties
26
+
27
+ If private repositories with personal data or customer code were readable, tell the owners of that data and check contractual and legal duties to notify.
28
+
29
+ ## Sources
30
+
31
+ - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation (accessed 2026-10-26)
32
+ - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # Google OAuth client secrets and API keys
2
+
3
+ Applies to rule `gcp-api-key` and to OAuth client secrets found by the generic rules.
4
+
5
+ ## 1. Revoke
6
+
7
+ For an API key, open **APIs & Services** > **Credentials** and regenerate or delete the key. For an OAuth client secret, add a new secret to the client, then disable the exposed one.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Restrict a new API key to the APIs it calls and to the referrers, IPs or apps that use it. For OAuth, keep the client and rotate only the secret. For server-to-server access prefer workload identity federation over service account key files.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the backend configuration, CI secrets and any mobile or web build that embeds the key.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Check API usage metrics and billing for the key's project in the window, and Cloud Audit Logs for calls you do not recognize.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ Delete the disabled OAuth secret once the new one has served traffic without errors.
24
+
25
+ ## 6. Notification duties
26
+
27
+ If the client could read user data through granted scopes, assess notice duties toward those users and the regulator.
28
+
29
+ ## Sources
30
+
31
+ - https://support.google.com/cloud/answer/6158849 (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # Mailgun API keys
2
+
3
+ Applies to Mailgun keys found by the generic rules (for example a `MAILGUN_KEY` assignment).
4
+
5
+ ## 1. Revoke
6
+
7
+ In the Mailgun control panel, open **API Security** and delete the exposed account key or domain sending key.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Prefer a domain sending key, which can only send mail for one domain, over an account key. Create an account key only for tools that manage the account, with the narrowest role.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the application configuration, CI secrets and any SMTP credentials that shared the leak.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Review sending logs and events for the window: mail you did not send, new domains, new routes or webhooks, and spikes in bounces or complaints that point to spam sent in your name.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ Remove any temporary key created for the switch after a few days without delivery errors.
24
+
25
+ ## 6. Notification duties
26
+
27
+ If message logs or mailing lists with personal data were readable, assess notice duties. If spam went out under your domain, warn the recipients' contact point and watch your domain reputation.
28
+
29
+ ## Sources
30
+
31
+ - https://documentation.mailgun.com/docs/mailgun/user-manual/api-key-mgmt/rbac-mgmt (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # OpenAI API keys
2
+
3
+ Applies to rule `openai-api-key`.
4
+
5
+ ## 1. Revoke
6
+
7
+ On the API keys page of the OpenAI platform, revoke the exposed key. Revocation takes effect at once.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Create a project key or a service account key in a project that holds only this application, with spend limits and, where the organization allows it, a maximum key lifetime.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the secret store, CI secrets and every service that reads `OPENAI_API_KEY`.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Review usage per project and per key in the window. Calls or spend you cannot explain mean the key was used by someone else.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ Remove any temporary key created for the switch once the new key has run cleanly for a few days.
24
+
25
+ ## 6. Notification duties
26
+
27
+ If the key could reach stored files, assistants or fine-tuning data with personal data, assess notice duties.
28
+
29
+ ## Sources
30
+
31
+ - https://developers.openai.com/api/docs/guides/production-best-practices (accessed 2026-10-26)
@@ -0,0 +1,31 @@
1
+ # Stripe secret and restricted keys
2
+
3
+ Applies to rule `stripe-access-token`.
4
+
5
+ ## 1. Revoke
6
+
7
+ On **Developers** > **API keys**, open the menu next to the key and choose **Roll key** with expiration **Now**, or **Expire key**. A rolled key with a later expiration keeps working until then, so choose **Now** for a leaked key.
8
+
9
+ ## 2. Issue the new credential
10
+
11
+ Create a restricted key (`rk_live_`) with only the permissions the integration needs and attach an access policy (IP addresses or allowed networks) so the key works only from your servers.
12
+
13
+ ## 3. Replace it in every consumer
14
+
15
+ Update the secret store or environment of every backend, worker and CI job. Webhook signing secrets are separate; roll them too if they were in the same file.
16
+
17
+ ## 4. Check for misuse in the exposure window
18
+
19
+ Open the request logs for the old key in Workbench and review the window: refunds, payouts, transfers, changes to payout bank accounts, new webhook endpoints and customer data exports.
20
+
21
+ ## 5. Delete the old credential after observation
22
+
23
+ If you rolled with a delay, confirm the old key's request volume stayed at zero, then let it expire or expire it now.
24
+
25
+ ## 6. Notification duties
26
+
27
+ Card data never passes through your keys, but customer names, emails and addresses do. If they were read, assess notice duties under the law that applies and tell Stripe support about suspicious activity.
28
+
29
+ ## Sources
30
+
31
+ - https://docs.stripe.com/keys (accessed 2026-10-26)