Masks API keys, tokens, private keys and password assignments in tool results before the model sees them.

Fourteen Claude Code mods, packaged as one plugin marketplace (qa-mods). Made for a manual QA workflow: guard rails around risky commands, evidence for what was tested, and a status band above the prompt.
Run in your own Terminal (a sandboxed Claude session cannot write ~/.claude).
claude plugin marketplace add oleksandrkodua/qa-mods
claude plugin install hud@qa-mods
Replace hud with any plugin from the table.
To install everything from a local clone, or to set up a fresh machine, see INSTALL.md and install-pack.sh.
| Plugin | What it does |
|---|---|
sandbox-guard | Denies writes to protected Claude config paths and hands back a Terminal command |
remote-gate | Asks before git push and wrangler --remote, showing folder, branch, remote and the commits that will go out |
blast-radius | Shows what a risky Bash command will touch before it runs |
retry-analyzer | Detects repeated identical failing tool calls and tells Claude to change strategy |
secret-redactor | Masks tokens and keys in tool output |
evidence-saver | Saves screenshots from tool results to a folder you pick |
replay-theater | /replay: step through the session's file edits one by one |
verification-guard | Checks claims like "tests pass" against tool calls that actually ran |
handoff | Offers /handoff as the context fills up; fills the prompt box with a HANDOFF.md prompt |
notify | A toast and a macOS notification when a long command or turn finishes |
quick-actions | Compact and Clear buttons, each asks to confirm first |
hud | Context fill and rate-limit windows above the prompt, a кеш 59:48 chip (m:ss) while the prompt cache is warm, five buttons, and /hud with the figures as text |
next-steps-uk | Fork of next-steps (MIT, Thariq Shihipar): suggested next prompts are always in Ukrainian. Install instead of next-steps |
plan-progress | Fork of plan-progress 0.7.6 (zycck, MIT): live progress bars above the prompt. Install instead of plan-progress@zycck-mods |
claude plugin test hud # from the repo root
cd hud && node --test tests/format.test.mjs tests/handoff-prompt.test.mjs
A bare node --test also picks up register.test.ts, which only runs under claude plugin test.
No license file for the original mods yet. The two forks keep their upstream MIT notices: next-steps-uk/NOTICE.md and plan-progress/NOTICE.md (plus plan-progress/LICENSE.upstream).
hooks/register.ts 40 lines1import type { Register } from 'claude-code'
2
3import { redactDeep } from './redact'
4
5const count = (v: unknown) => (JSON.stringify(v) ?? '').split('[REDACTED').length - 1
6
7/**
8 * Secret Redactor: masks secrets in every tool result before the model reads it.
9 * A changed result is returned as the hook's own `{ result, context }` (no `ref`/`text`),
10 * so the engine re-maps it for the model; `context` asks the model to tell the user.
11 */
12export const register: Register = on => {
13 on('tool.call', async ($, e, next) => {
14 const ran = await next(e)
15
16 if (ran.deny !== undefined) return ran
17
18 // A blocked call (e.g. a PreToolUse guard) carries its message as a string `result`; the engine
19 // checks `result` against the tool's output shape (an object), so only redact object results.
20 if (ran.isError === true) {
21 const isObject = typeof ran.result === 'object' && ran.result !== null
22
23 return { ...ran, result: isObject ? redactDeep(ran.result) : ran.result, text: redactDeep(ran.text) }
24 }
25
26 const result = redactDeep(ran.result)
27 const masked = count(result) - count(ran.result)
28
29 if (masked <= 0) return ran
30
31 return {
32 result,
33 context: [
34 ...(ran.context ?? []),
35 `secret-redactor: ${masked} secret value(s) were masked as [REDACTED] in this tool output. Tell the user briefly.`,
36 ],
37 }
38 })
39}
40hooks/redact.ts 38 lines1const PATTERNS: [RegExp, string][] = [
2 [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED:private-key]'],
3 [/\bsk-ant-[A-Za-z0-9_-]{20,}/g, '[REDACTED:anthropic-key]'],
4 [/\bsk-[A-Za-z0-9_-]{32,}/g, '[REDACTED:api-key]'],
5 [/\bgh[pousr]_[A-Za-z0-9]{30,}/g, '[REDACTED:github-token]'],
6 [/\bgithub_pat_[A-Za-z0-9_]{40,}/g, '[REDACTED:github-token]'],
7 [/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:slack-token]'],
8 [/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED:aws-key-id]'],
9 [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[REDACTED:jwt]'],
10 [/(\b(?:authorization|bearer)\s*[:=]?\s*bearer\s+)[A-Za-z0-9._~+/=-]{16,}/gi, '$1[REDACTED]'],
11]
12
13/** `token = getToken(x)`, `secret: process.env.X`, `password = user.password`: code, not a secret value. */
14const looksLikeCode = (v: string) =>
15 /[(){}<>]|=>|^[$@]|^(?:process|os|env|self|this|config|args|opts?)\./i.test(v) ||
16 /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+$/.test(v) ||
17 /^\[REDACTED/.test(v)
18
19const ASSIGN = /(\b[A-Za-z0-9_]*(?:password|passwd|secret|token|api[_-]?key)[A-Za-z0-9_]*["']?\s*[:=]\s*["']?)([^\s"']{6,})/gi
20
21export function redact(s: string): string {
22 return PATTERNS.reduce((acc, [re, sub]) => acc.replace(re, sub), s).replace(ASSIGN, (m, k, v) =>
23 looksLikeCode(v) ? m : `${k}[REDACTED]`,
24 )
25}
26
27/** Deep-map every string in a result; leaves non-strings and shape intact. */
28export function redactDeep<T>(v: T): T {
29 if (typeof v === 'string') return redact(v) as T
30 if (Array.isArray(v)) return v.map(redactDeep) as T
31
32 if (v && typeof v === 'object') {
33 return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, redactDeep(x)])) as T
34 }
35
36 return v
37}
38