SLOPSHOPPER

secret-sentry

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

newbandguard
A shopper browsing a rack in a slop shop
README

secret-sentry

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

Install

claude plugin marketplace add kenhuangus/claude-code-mods-pack
claude plugin install secret-sentry@kenhuang-mods
Source 2 files
hooks/secret-sentry.mjs 40 lines
1const 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}
40
types/index.d.ts 8 lines
1declare module "claude-code" {
2  interface PluginState {
3    "secret-sentry": {
4      hits: { blocked: number; lastReason?: string };
5    };
6  }
7}
8