Blocks Write/Edit tool calls that contain API keys, PEM blocks, or .env secrets.

Blocks Write/Edit tool calls that contain API keys, PEM blocks, or .env secrets.
claude plugin marketplace add kenhuangus/claude-code-mods-pack
claude plugin install secret-sentry@kenhuang-modshooks/secret-sentry.mjs 40 lines1const HITS = { plugin: "secret-sentry", key: "hits" };
2const RULES = [
3 { re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, reason: "private-key-pem" },
4 { re: /\b(?:sk|pk)-(?:live|test|proj)-[A-Za-z0-9]{16,}\b/, reason: "vendor-api-key" },
5 { re: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/, reason: "aws-access-key" },
6 { re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/, reason: "github-token" },
7 { re: /(?:^|\n)\s*(?:API_KEY|SECRET_KEY|PASSWORD|TOKEN|OPENAI_API_KEY)\s*=\s*['"]?[^'\n\r]{8,}/i, reason: "env-assignment" },
8];
9export function findSecret(text) {
10 const s = String(text ?? "");
11 for (const rule of RULES) {
12 if (rule.re.test(s)) return rule.reason;
13 }
14 return null;
15}
16async function guardWrite($, e, next) {
17 const content = e.content ?? e.new_string ?? e.newString ?? e.payload ?? "";
18 const reason = findSecret(content);
19 if (!reason) return next(e);
20 const { value: hits } = await $.state.get(HITS);
21 await $.state.set(HITS, { blocked: (hits?.blocked ?? 0) + 1, lastReason: reason });
22 return { deny: `Secret Sentry blocked write (${reason}). Use env vars / secret manager instead of plaintext credentials.` };
23}
24export function register(on) {
25 on("session.start", async ($, e, next) => {
26 const r = await next(e);
27 await $.state.set(HITS, { blocked: 0 });
28 return r;
29 });
30 on("tool.call", { tool: "Write" }, guardWrite);
31 on("tool.call", { tool: "Edit" }, guardWrite);
32 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
33 if (e.props?.hasSurvey) return next(e);
34 const { value: hits } = await $.state.get(HITS);
35 if (!hits?.blocked) return next(e);
36 const { Box, Text } = $.ui.resolve(e);
37 return Box({ paddingX: 1, children: [Text({ color: "red", bold: true, children: `SECRET-SENTRY blocked:${hits.blocked}` })] });
38 });
39}
40types/index.d.ts 8 lines1declare module "claude-code" {
2 interface PluginState {
3 "secret-sentry": {
4 hits: { blocked: number; lastReason?: string };
5 };
6 }
7}
8