SLOPSHOPPER

secret-redactor

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

newguard
v0.1.5no licenseupdated 2026-10-08oleksandrkodua/qa-mods/secret-redactor
A shopper browsing a rack in a slop shop
README

qa-mods

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.

Install

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.

Plugins

PluginWhat it does
sandbox-guardDenies writes to protected Claude config paths and hands back a Terminal command
remote-gateAsks before git push and wrangler --remote, showing folder, branch, remote and the commits that will go out
blast-radiusShows what a risky Bash command will touch before it runs
retry-analyzerDetects repeated identical failing tool calls and tells Claude to change strategy
secret-redactorMasks tokens and keys in tool output
evidence-saverSaves screenshots from tool results to a folder you pick
replay-theater/replay: step through the session's file edits one by one
verification-guardChecks claims like "tests pass" against tool calls that actually ran
handoffOffers /handoff as the context fills up; fills the prompt box with a HANDOFF.md prompt
notifyA toast and a macOS notification when a long command or turn finishes
quick-actionsCompact and Clear buttons, each asks to confirm first
hudContext 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-ukFork of next-steps (MIT, Thariq Shihipar): suggested next prompts are always in Ukrainian. Install instead of next-steps
plan-progressFork of plan-progress 0.7.6 (zycck, MIT): live progress bars above the prompt. Install instead of plan-progress@zycck-mods

Tests

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.

License

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).

Source 2 files
hooks/register.ts 40 lines
1import 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}
40
hooks/redact.ts 38 lines
1const 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