Scrubs API keys, tokens and private keys from prompts and tool output before the model sees them.

Eleven mods for Claude Code. Install the ones you want; each is independent.

repo-radar draws your repo as a live map of rectangles, one per folder. Reads glow cyan, edits amber, failures red, and each subagent gets its own colour, so you can watch parallel agents work across your codebase. Glows fade like a radar trace; files touched this session keep a faint tint. /radar opens it. More →
/plugin marketplace add lakmadev/claude-mods
/plugin install repo-radar@claude-mods
![]()
| Mod | What it does |
|---|---|
| pixel-pet | A tiny animated pixel pet above your prompt. It sleeps (snoring) when things are quiet and wakes slowly when you type. It narrates what Claude is doing with a sliding status line, hatches half-size helpers in their own colours for each subagent that slide into the meter line one by one and wave bye when done. It watches you type, and you click it to pet it. A gradient context meter with your 5-hour and weekly usage sits beside it, details on hover. 28 colours via /pet color <name>. |
| code-wrapped | /wrapped opens your Claude Code Wrapped: a GitHub-style activity heatmap, hours, lines, streaks, top files and tools, your coding personality (Night Owl 🦉, Refactorer 🧹, Shell Wizard 🧙…) and 14 unlockable achievements that pop as toasts. Copy share text puts a one-line brag on your clipboard. |
| lofi-mode | Soft lo-fi beats start when Claude has been working for a few seconds and stop when it finishes. The loop is original and synthesized for this mod. /lofi toggles it. macOS audio. |
| Mod | What it does | |
|---|---|---|
| context-meter | A plain status line: ctx 62% · 5h 85% resets 1h52m · week 40% on a Claude plan (the reset shows once a window passes 70%), or ctx 62% · $1.84 billed (API) when you pay per token. Toasts when context passes 80% or a plan window passes 90%. | |
| safety-net | Refuses catastrophic commands (rm -rf ~, mkfs, dd of=/dev/disk, fork bombs). Asks before risky ones (force-push, reset --hard, `curl \ | sh, DROP TABLE, terraform destroy, sudo, …) and before edits to .env, keys, ~/.ssh, .git/` or shell rc files. |
| changes-pane | Side pane listing every file Claude changed this session with +/- line counts. /changes opens it. | |
| done-notifier | Chime, toast and desktop notification when a turn longer than 30s finishes or Claude is waiting for you. | |
| prompt-booster | Short prompts ("fix it") get your branch, changed files and recent commits as hidden context. Adds /fix-tests, /explain-code, /review-changes, /write-tests, /commit-msg. | |
| session-journal | Logs each turn (project, prompt, files touched) across sessions. /journal [days] lists it; /standup [days] writes a Done / In progress / Blockers update. | |
| secrets-redactor | Replaces API keys, tokens, private keys and connection-string passwords with [REDACTED:kind] in your prompts and in tool output before the model sees them, and blocks writing a placeholder back over a real secret. |
Requires Claude Code 2.1.293 or later: mods are an early-access feature and gain events with each release. Check with claude --version; update with claude update.
In a Claude Code terminal session:
/plugin marketplace add lakmadev/claude-mods
/plugin install pixel-pet@claude-mods
The first line adds this repo as a plugin marketplace (once); the second installs a mod from it. Repeat the second line with any other mod name from the table.
Each mod's options show in /config once it is installed:
contextWarnPercent (80), rateLimitWarnPercent (90)strict (off). Turns every "ask" into a refusal. Turn it on if you run in auto or bypass-permissions mode, where an ask can be approved without you.autoOpen (on). The pane opens on its own only in terminals 144+ columns wide.minSeconds (30), sound (on), desktop (on)autoContext (on), maxWords (15)retentionDays (30), model (haiku)aggressive (off). Also redacts quoted password = "…"-style assignments.name (Bit), color (teal)volume (0.35), delaySeconds (4)eval, base64, aliases) get through.osascript (macOS) or notify-send (Linux)./standup sends the log to the model you configure.Each mod lives in plugins/<name>/: hooks/register.ts(x) is the code and tests/ holds its tests.
claude plugin validate plugins/<name>
claude plugin test plugins/<name>
claude --plugin-dir plugins/<name>
MIT, see LICENSE.
hooks/register.ts 95 lines1import type { Register } from 'claude-code'
2
3const MARK = '[REDACTED:'
4
5const PATTERNS: [kind: string, pattern: RegExp][] = [
6 ['private-key', /-----BEGIN [A-Z ]*PRIVATE KEY( BLOCK)?-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY( BLOCK)?-----/g],
7 ['aws-access-key', /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g],
8 ['github-token', /\b(gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/g],
9 ['gitlab-token', /\bglpat-[A-Za-z0-9_-]{20,}\b/g],
10 ['slack-token', /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g],
11 ['stripe-key', /\b[sr]k_live_[A-Za-z0-9]{20,}\b/g],
12 ['google-api-key', /\bAIza[0-9A-Za-z_-]{35}\b/g],
13 ['anthropic-key', /\bsk-ant-[A-Za-z0-9_-]{20,}/g],
14 ['openai-key', /\bsk-(proj-|svcacct-)?[A-Za-z0-9_-]{32,}/g],
15 ['jwt', /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g],
16 ['npm-token', /\bnpm_[A-Za-z0-9]{36}\b/g],
17]
18
19// user:password@ in connection strings; only the password goes.
20const URL_PASSWORD = /\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)([^\s@/]{3,})(@)/gi
21const AWS_SECRET = /(aws_secret_access_key\s*[=:]\s*["']?)([A-Za-z0-9/+=]{40})/gi
22const ASSIGNMENT = /((?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret)["']?\s*[:=]\s*)(["'])([^"'\s]{8,})\2/gi
23
24export function redact(text: string, aggressive = false): { text: string; count: number } {
25 let count = 0
26 let out = text
27 for (const [kind, pattern] of PATTERNS) {
28 out = out.replace(pattern, () => (count++, `${MARK}${kind}]`))
29 }
30 out = out.replace(URL_PASSWORD, (_, head, __, at) => (count++, `${head}${MARK}password]${at}`))
31 out = out.replace(AWS_SECRET, (_, head) => (count++, `${head}${MARK}aws-secret]`))
32 if (aggressive) out = out.replace(ASSIGNMENT, (_, head, quote) => (count++, `${head}${quote}${MARK}secret]${quote}`))
33 return { text: out, count }
34}
35
36type Block = { type: string; [field: string]: unknown }
37
38// Rewrites text blocks and tool_result contents (string or nested text blocks); everything else passes as is.
39export function redactBlocks(blocks: readonly Block[], aggressive: boolean): { blocks: Block[]; count: number } {
40 let count = 0
41 const text = (value: string) => {
42 const result = redact(value, aggressive)
43 count += result.count
44 return result.text
45 }
46 const walk = (block: Block): Block => {
47 if (block.type === 'text' && typeof block.text === 'string') return { ...block, text: text(block.text) }
48 if (block.type !== 'tool_result') return block
49 if (typeof block.content === 'string') return { ...block, content: text(block.content) }
50 if (Array.isArray(block.content)) return { ...block, content: (block.content as Block[]).map(walk) }
51 return block
52 }
53 const out = blocks.map(walk)
54 return { blocks: out, count }
55}
56
57export const register: Register = (on, options) => {
58 const aggressive = options.aggressive === true
59
60 on('prompt.submit', async ($, e, next) => {
61 const { text, count } = redact(e.text, aggressive)
62 if (count === 0) return next(e)
63 $.ui.toast(`secrets-redactor: removed ${count} secret(s) from your prompt before sending.`)
64 return next({ ...e, text })
65 }).catch(($, e, next) => (next.called ? next(e) : { drop: 'secrets-redactor could not scan this prompt, so it was not sent.' }))
66
67 on('session.append', { door: 'tool-result' }, async ($, e, next) => {
68 const { blocks, count } = redactBlocks(e.message.content, aggressive)
69 if (count === 0) return next(e)
70 $.ui.status(`secrets-redactor: ${count} secret(s) hidden from Claude`)
71 return next({ ...e, message: { ...e.message, content: blocks } })
72 }).catch(($, e, next) =>
73 // Could not scan it: withhold the output rather than risk leaking it.
74 next({
75 ...e,
76 message: {
77 ...e.message,
78 content: e.message.content.map(block =>
79 block.type === 'tool_result' ? { ...block, content: 'Output withheld: secrets-redactor could not scan it.', is_error: true } : block,
80 ),
81 },
82 }),
83 )
84
85 // A redacted value written back to disk would destroy the real secret.
86 on('tool.check', async ($, e, next) => {
87 const args = (e.input ?? {}) as Record<string, unknown>
88 const written = [args.content, args.new_string, args.new_source, args.command].filter(v => typeof v === 'string') as string[]
89 if (written.some(v => v.includes(MARK))) {
90 return { decision: 'deny', reason: `secrets-redactor: this would write a ${MARK}…] placeholder over a real secret. Ask the user to make this change themselves.` }
91 }
92 return next(e)
93 }).catch(($, e, next) => (next.called ? next(e) : { decision: 'ask', reason: 'secrets-redactor could not check this call.' }))
94}
95