SLOPSHOPPER

secret-guard

Hides API keys, tokens and passwords from Claude: every tool result is scanned and secrets are replaced before Claude reads them. Shows a live count above the…

bandguardcommand
v0.1.0MITupdated 2026-10-17MahammadHasanli/secret-guard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · secret-guard
› fix the failing auth test and add an audit log call ● secret-guard: hid 1 secret from Claude (Bash) ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /secrets ⎿ secret-guard: hid 1 secret from Claude this session: 1 × Stripe key ■ secret-guard 1 secret hidden from Claude last: Stripe key in Bash output ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
■ secret-guard 1 secret hidden from Claude last: Stripe key in Bash output
README

secret-guard

A Claude Code mod that hides API keys, tokens and passwords from Claude.

Every time Claude uses a tool (reads a file, runs a command, searches), secret-guard lets the tool run, scans everything it returned, and replaces secrets with [hidden by secret-guard] before Claude reads it. A line above the prompt shows how many secrets were hidden. /secrets lists what kinds were hidden (never the values).

■ secret-guard  4 secrets hidden from Claude   last: env secret in Read output

What it hides

  • Anthropic keys (sk-ant-…), OpenAI keys (sk-…), GitHub tokens (ghp_…), Stripe live keys (sk_live_…), AWS keys (AKIA…), Google keys (AIza…), Slack tokens (xox…), private keys (-----BEGIN … PRIVATE KEY-----)
  • any NAME_KEY=…, …_TOKEN=…, …_SECRET=…, …_PASSWORD=… line (e.g. in .env files)

If scanning ever fails, the result is withheld (fails closed).

Install

Needs Claude Code 2.1.287 or newer (claude --version). Tested on 2.1.296.

git clone https://github.com/MahammadHasanli/secret-guard
claude --plugin-dir ./secret-guard

Limits

  • It only covers what Claude reads through tools. Text you paste into the prompt yourself is not scanned.
  • Pattern-based: a secret with an unusual format can slip through. It is a safety net, not a vault.
  • Mods are not sandboxed. Read hooks/secret-guard.mjs (about 100 lines) before you install it.

Check it

claude plugin validate ./secret-guard
cd secret-guard && claude plugin test

Made by LatestAgent.

Source 1 files
hooks/secret-guard.mjs 99 lines
1// secret-guard: hide secrets from Claude.
2//
3// tool.call: let the tool run, then scan everything it returned and replace API keys,
4// tokens and passwords with a placeholder before Claude reads the result.
5// ui.render (AbovePrompt): one line with how many secrets were hidden this session.
6// /secrets: print what was hidden (kinds only, never the values).
7//
8// The host reads on(...) and $.noun.method(...) from source, so they are spelled
9// literally, and helpers that take $ are top-level functions.
10
11const RULES = [
12  { kind: "Anthropic key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
13  { kind: "OpenAI key", re: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
14  { kind: "GitHub token", re: /gh[pousr]_[A-Za-z0-9]{30,}/g },
15  { kind: "AWS key", re: /AKIA[0-9A-Z]{16}/g },
16  { kind: "Google key", re: /AIza[0-9A-Za-z_-]{35}/g },
17  { kind: "Slack token", re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
18  { kind: "Stripe key", re: /(?:sk|rk)_live_[A-Za-z0-9]{20,}/g },
19  { kind: "private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
20  // NAME_KEY=value / NAME_TOKEN: value lines, e.g. in .env files and configs
21  { kind: "env secret", re: /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)[A-Z0-9_]*)(\s*[=:]\s*)("?)(?!\[hidden)([^\s"'#]{6,})\3/g, keepName: true },
22];
23const MASK = "[hidden by secret-guard]";
24
25let hidden = 0;
26const seen = new Set(); // each secret counts once, however often it shows up
27let last = "";
28const kinds = {};
29
30export function register(on) {
31  on("session.start", async ($, e, next) => {
32    const result = await next(e);
33    await $.command.register({ name: "secrets", description: "secret-guard: what was hidden from Claude this session" });
34    return result;
35  });
36
37  on("tool.call", async ($, e, next) => {
38    const result = await next(e);
39    const before = hidden;
40    const clean = scrub(result, e.tool);
41    if (hidden > before) {
42      $.ui.log(`hid ${hidden - before} secret${hidden - before > 1 ? "s" : ""} from Claude (${e.tool})`);
43      $.ui.invalidate("ui.render");
44    }
45    return clean;
46  }).catch(async ($, e, next) => {
47    // fail closed: if scanning broke, Claude gets nothing rather than a possible secret
48    if (next.called) return { deny: "secret-guard could not scan this result, so it was withheld." };
49    return next(e);
50  });
51
52  on("command.run", { command: "secrets" }, async () => {
53    if (!hidden) return { text: "no secrets seen yet" };
54    const list = Object.entries(kinds).map(([k, n]) => `${n} × ${k}`).join(", ");
55    return { text: `hid ${hidden} secret${hidden > 1 ? "s" : ""} from Claude this session: ${list}` };
56  });
57
58  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
59    if (e.hasSurvey) return next(e);
60    const { Box, Text } = $.ui.resolve(e);
61    const parts = [
62      Text({ color: hidden ? "green" : "gray", bold: true, children: "■ secret-guard" }),
63      Text({ children: hidden ? `  ${hidden} secret${hidden > 1 ? "s" : ""} hidden from Claude` : "  watching tool output" }),
64    ];
65    if (last && (e.bodyColumns ?? 80) >= 70) parts.push(Text({ dimColor: true, children: `   last: ${last}` }));
66    return Box({ flexDirection: "row", paddingX: 1, children: parts });
67  });
68}
69
70// Walk any result shape and redact every string in it.
71function scrub(value, tool) {
72  if (typeof value === "string") return redact(value, tool);
73  if (Array.isArray(value)) return value.map((v) => scrub(v, tool));
74  if (value && typeof value === "object") {
75    const out = {};
76    for (const [k, v] of Object.entries(value)) out[k] = scrub(v, tool);
77    return out;
78  }
79  return value;
80}
81
82function redact(text, tool) {
83  let out = text;
84  for (const rule of RULES) {
85    out = out.replace(rule.re, (...m) => {
86      if (m[0].includes(MASK)) return m[0];
87      const value = rule.keepName ? m[4] : m[0];
88      if (!seen.has(value)) {
89        seen.add(value);
90        hidden += 1;
91        kinds[rule.kind] = (kinds[rule.kind] ?? 0) + 1;
92        last = `${rule.kind} in ${tool} output`;
93      }
94      return rule.keepName ? `${m[1]}${m[2]}${MASK}` : MASK;
95    });
96  }
97  return out;
98}
99