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…

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
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-----)NAME_KEY=…, …_TOKEN=…, …_SECRET=…, …_PASSWORD=… line (e.g. in .env files)If scanning ever fails, the result is withheld (fails closed).
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
hooks/secret-guard.mjs (about 100 lines) before you install it.claude plugin validate ./secret-guard
cd secret-guard && claude plugin test
Made by LatestAgent.
hooks/secret-guard.mjs 99 lines1// 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