/rec before you record: masks keys, personal details, and business figures on screen and keeps private files closed (/rec strict, /rec off, /rec config)

<h1 align="center">Awesome Claude Code Mods</h1>
<a id="showcase"></a>
<table> <tr><td width="33%" align="center" valign="top"><p><a href="https://github.com/therahul-yo/clawdman"><img src="assets/showcase-01.gif" width="100%" alt="Clawdman: An animated companion that reacts to your coding session. Demo · 10×."></a></p><p><strong><a href="https://github.com/therahul-yo/clawdman">Clawdman</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="AgentMods/pi-agent/"><img src="assets/showcase-02.gif" width="100%" alt="Pi Agent: Run pi-powered models as native Claude Code subagents. Demo · 10×."></a></p><p><strong><a href="AgentMods/pi-agent/">Pi Agent</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="FocusMods/breathing-exercises/"><img src="assets/showcase-03.gif" width="100%" alt="Breathing Exercises: A guided breathing animation while Claude works."></a></p><p><strong><a href="FocusMods/breathing-exercises/">Breathing Exercises</a></strong></p></td></tr> <tr><td width="33%" align="center" valign="top"><p><a href="sources/creative-toolkit/"><img src="assets/showcase-04.png" width="100%" alt="Creative Toolkit: A collection of live panels, session tools and playful mods. Code Pet screenshot."></a></p><p><strong><a href="sources/creative-toolkit/">Creative Toolkit</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="GameMods/agent-cartoons/"><img src="assets/showcase-05.gif" width="100%" alt="Agent Cartoons: Coding activity becomes a cartoon in a choice of visual styles."></a></p><p><strong><a href="GameMods/agent-cartoons/">Agent Cartoons</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="https://github.com/adamholter/claude-subway-surfers"><img src="assets/showcase-06.gif" width="100%" alt="Subway Surfers Desktop: Gameplay beside Claude Desktop while it works. Desktop demo · 10×."></a></p><p><strong><a href="https://github.com/adamholter/claude-subway-surfers">Subway Surfers Desktop</a></strong></p></td></tr> <tr><td width="33%" align="center" valign="top"><p><a href="GameMods/dino-game/"><img src="assets/showcase-07.gif" width="100%" alt="Dino Game: Play the T-rex runner above your prompt."></a></p><p><strong><a href="GameMods/dino-game/">Dino Game</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="GameMods/spell-bar/"><img src="assets/showcase-08.gif" width="100%" alt="Spell Bar: An animated spell bar for your selected effort level."></a></p><p><strong><a href="GameMods/spell-bar/">Spell Bar</a></strong></p></td><td width="33%" align="center" valign="top"><p><a href="AgentMods/agent-dashboard/"><img src="assets/showcase-09.gif" width="100%" alt="Agent Dashboard: A live dashboard for context, costs, permissions and agents."></a></p><p><strong><a href="AgentMods/agent-dashboard/">Agent Dashboard</a></strong></p></td></tr> </table>
<a id="browse-by-category"></a>
<h2>Browse by category <img src="assets/category-magnifier.gif" width="52" height="40" alt="Clawd with a magnifying glass"></h2>
Usage Mods · Agent Mods · Planning Mods · File Mods · Git Mods · Safety Mods · Memory Mods · Interface Mods · Prompt Mods · Testing Mods · Web Mods · Focus Mods · Game Mods · Integration Mods
Source packages available here: 82 mods from 38 repositories, with their complete tracked files and notices. Local package links open inside this collection. Other entries currently lead to upstream references while the wider source import is reviewed.
Entries are grouped by their main purpose. Collections can contain several kinds of mods; hosted mod pages are categorized individually.
<a id="usage"></a>
Track model usage, costs, effort and quotas.
<a id="agents"></a>
Coordinate agents and follow their actual work.
hooks/register.mjs 279 lines1// Recording Mode: /rec before you hit record.
2// While it is on, in every Claude Code session on this machine:
3// - keys, .env values, emails, names, phone numbers, addresses, ID and account numbers,
4// money, and business figures are masked wherever the transcript draws text
5// - results from mail, chat, tasks, files, calendar, and finance tools draw as hidden
6// - Claude can't open private files (.env files, credentials, Claude's memory, finance
7// documents, and anything you list in the config file) or finance tools, and each
8// prompt tells it to keep plans and figures out of its replies
9// - a pulsing red ● REC above the prompt reminds you it's on
10// /rec strict also masks every large figure and percentage. /rec off turns it all off.
11// /rec config makes ~/.claude/mods-data/recording-mode/config.json for your own name,
12// the people to hide, your private folders, and extra tools.
13// It changes what is drawn and what Claude may open; the note to Claude is the one thing it adds.
14
15import { makeMasker, deepMask, hideText, privatePathHit, toolCallTargets, secretValuesFromEnv, businessSource, financeTool } from './privacy.mjs'
16
17const FLAG_PATH = '/.claude/mods-data/recording.json'
18const CONFIG_PATH = '/.claude/mods-data/recording-mode/config.json'
19
20const CONFIG_TEMPLATE = {
21 keepNames: ['Your Name'],
22 names: ['A Teammate', 'A Client'],
23 privatePaths: ['clients/', 'finance/', 'my-private-notes.md'],
24 businessTools: [],
25 closedTools: [],
26}
27
28const CONFIG_HELP = [
29 'keepNames: your own name (and your brand), shown even while recording.',
30 'names: people whose names are always masked (teammates, clients). Names in emails and contact fields are learned on their own.',
31 'privatePaths: folders or file names Claude may not open while recording. Any part of a path, any case.',
32 'businessTools: tool or command names (any part) whose results draw as hidden, beyond the built-in list.',
33 'closedTools: tool names (any part) Claude may not call while recording, beyond the built-in finance tools.',
34]
35
36// Drawn as a plain image: an interactive Svg sits in an iframe that paints an
37// opaque white box in the dark theme and reloads on every band redraw.
38// SMIL still runs in an image. One fading dot, no ring, so a restart barely shows.
39const REC_RED = '#e5484d'
40const REC_SVG =
41 '<svg xmlns="http://www.w3.org/2000/svg" width="54" height="18" viewBox="0 0 54 18">' +
42 `<circle cx="8" cy="9" r="5" fill="${REC_RED}"><animate attributeName="opacity" values="1;0.35;1" dur="1.6s" repeatCount="indefinite" calcMode="spline" keyTimes="0;0.5;1" keySplines="0.45 0 0.55 1;0.45 0 0.55 1"/></circle>` +
43 `<text x="19" y="13.2" font-family="-apple-system,'Segoe UI',system-ui,sans-serif" font-size="12" font-weight="700" letter-spacing="0.8" fill="${REC_RED}">REC</text>` +
44 '</svg>'
45
46let home = ''
47let cwd = ''
48let commandName = 'rec'
49let rec = { on: false, strict: false, since: 0 }
50let values = [] // .env secret values: kept in memory only, never written anywhere
51let config = { keepNames: [], names: [], privatePaths: [], businessTools: [], closedTools: [] }
52let mask = (s) => s
53let blocked = 0
54let noteSent = false
55const businessCalls = new Set() // tool_use_ids whose results are business data
56
57function strings(v) {
58 return Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x.trim()).map((x) => x.trim()).slice(0, 500) : []
59}
60
61function noteOn() {
62 const keep = config.keepNames.length ? ` (other than ${config.keepNames.join(', ')})` : ''
63 return (
64 `Recording mode is on: this screen is being recorded for a public video. In replies and in tool commands, leave out people's names${keep}, emails, phone numbers, addresses, account numbers, dollar amounts, revenue, profit, pricing, compensation, and other business figures, and internal plans, strategy, deals, hiring, and team matters. ` +
65 'Write placeholders like [name], [amount], or [internal plan] instead and keep summaries high level. Private files and finance tools stay closed until recording stops.'
66 )
67}
68const NOTE_OFF = 'Recording mode is off now. The earlier recording-mode note no longer applies.'
69
70function apply() {
71 mask = rec.on ? makeMasker({ strict: rec.strict, values, names: config.names, keepNames: config.keepNames }) : (s) => s
72}
73
74async function load($) {
75 await loadEnvValues($)
76 await loadConfig($)
77}
78
79function unload() {
80 values = []
81 businessCalls.clear()
82}
83
84async function loadConfig($) {
85 try {
86 const path = home + CONFIG_PATH
87 if (!(await $.fs.exists(path))) return
88 const raw = JSON.parse(await $.fs.read(path))
89 config = {
90 keepNames: strings(raw.keepNames),
91 names: strings(raw.names),
92 privatePaths: strings(raw.privatePaths),
93 businessTools: strings(raw.businessTools),
94 closedTools: strings(raw.closedTools),
95 }
96 } catch {
97 // a broken config file: the built-in rules still apply
98 }
99}
100
101function parentDirs(path, levels) {
102 const parts = String(path || '').replace(/\\/g, '/').split('/')
103 const out = []
104 for (let i = parts.length; i > 0 && out.length <= levels; i--) out.push(parts.slice(0, i).join('/'))
105 return out.filter(Boolean)
106}
107
108async function loadEnvValues($) {
109 const found = []
110 for (const dir of parentDirs(cwd, 3)) {
111 for (const name of ['.env', '.env.local']) {
112 const p = dir + '/' + name
113 try {
114 if (await $.fs.exists(p)) found.push(...secretValuesFromEnv(await $.fs.read(p)))
115 } catch {
116 // unreadable: patterns still catch the common key formats
117 }
118 }
119 }
120 values = [...new Set(found)].sort((a, b) => b.length - a.length)
121}
122
123async function readFlag($) {
124 try {
125 const path = home + FLAG_PATH
126 if (!(await $.fs.exists(path))) return { on: false, strict: false, since: 0 }
127 const flag = JSON.parse(await $.fs.read(path))
128 return { on: !!flag.on, strict: !!flag.strict, since: flag.since || 0 }
129 } catch {
130 return { on: false, strict: false, since: 0 }
131 }
132}
133
134// Another session may have turned recording on or off
135async function syncFlag($) {
136 const flag = await readFlag($)
137 if (flag.on === rec.on && flag.strict === rec.strict) return
138 rec = flag
139 if (rec.on) await load($)
140 else unload()
141 apply()
142 $.ui.invalidate('ui.render')
143}
144
145async function setFlag($, on, strict) {
146 rec = { on, strict: on && strict, since: on ? await $.clock.now() : 0 }
147 await $.fs.write(home + FLAG_PATH, JSON.stringify(rec))
148 if (rec.on) await load($)
149 else unload()
150 apply()
151 $.ui.invalidate('ui.render')
152}
153
154// /rec config: make the file the first time, then say where it is and what goes in it
155async function configText($) {
156 const path = home + CONFIG_PATH
157 let made = false
158 try {
159 if (!(await $.fs.exists(path))) {
160 await $.fs.write(path, JSON.stringify(CONFIG_TEMPLATE, null, 2) + '\n')
161 made = true
162 }
163 } catch {
164 return `Could not write ${path}. Make it by hand with this shape:\n${JSON.stringify(CONFIG_TEMPLATE, null, 2)}`
165 }
166 await loadConfig($)
167 apply()
168 return [
169 `${made ? 'Made' : 'Your'} recording config: ${path}`,
170 made ? 'It holds example values. Replace them with your own, save, and run /rec again.' : `Loaded: ${config.keepNames.length} kept name(s), ${config.names.length} hidden name(s), ${config.privatePaths.length} private path(s).`,
171 ...CONFIG_HELP,
172 ].join('\n')
173}
174
175async function registerCommand($) {
176 const spec = { name: 'rec', description: 'Recording mode: hide keys, personal details, business figures, and private files on screen (/rec strict, /rec off, /rec config)', argumentHint: '[strict|off|config]', immediate: true }
177 try {
178 await $.command.register(spec)
179 return 'rec'
180 } catch {
181 try {
182 await $.command.register({ ...spec, name: 'recording' })
183 return 'recording'
184 } catch {
185 return null
186 }
187 }
188}
189
190export function register(on) {
191 on('session.start', async ($, e, next) => {
192 home = (await $.env.get('USERPROFILE')) || (await $.env.get('HOME')) || ''
193 cwd = await $.session.cwd()
194 commandName = (await registerCommand($)) || commandName
195 await loadConfig($)
196 await syncFlag($)
197 $.clock.every(3000, () => syncFlag($).catch(() => {}))
198 return next(e)
199 })
200
201 on('command.run', { command: ['rec', 'recording'] }, async ($, e) => {
202 const arg = String(e.args || '').trim().toLowerCase()
203 if (arg === 'config') return { text: await configText($) }
204 if (arg === 'off' || (arg === '' && rec.on)) {
205 await setFlag($, false, false)
206 $.ui.toast('Recording mode off.' + (blocked ? ` It kept ${blocked} private file${blocked === 1 ? '' : 's'} closed.` : ''))
207 blocked = 0
208 return {}
209 }
210 const strict = arg === 'strict'
211 await setFlag($, true, strict)
212 $.ui.toast(`Recording mode on${strict ? ' (strict)' : ''}.`, { timeoutMs: 3000 })
213 return {}
214 })
215
216 // Claude reads a note beside each prompt while recording, and one more when it stops
217 on('prompt.submit', async ($, e, next) => {
218 const note = rec.on ? noteOn() : noteSent ? NOTE_OFF : null
219 if (!note) return next(e)
220 noteSent = rec.on
221 return next({ ...e, context: [...(e.context ?? []), note] })
222 })
223
224 // Private files and finance tools stay closed while recording
225 on('tool.call', async ($, e, next) => {
226 if (!rec.on) return next(e)
227 const hit = financeTool(e.tool, config.closedTools) ? 'finance tools' : privatePathHit(toolCallTargets(e).join('\n'), config.privatePaths)
228 if (!hit) {
229 if (businessSource(e.tool, e, config.businessTools)) businessCalls.add(e.tool_use_id)
230 return next(e)
231 }
232 blocked += 1
233 return {
234 deny: `Recording mode is on, so "${hit}" stays closed while the screen is being recorded. Continue without it, or ask the user to run /${commandName} off first.`,
235 }
236 })
237
238 // Text rows: prompts, replies, command output
239 on('ui.render', { component: ['UserMessage', 'AssistantMessage', 'CommandOutput'] }, async ($, e, next) => {
240 if (!rec.on || typeof e.props.text !== 'string') return next(e)
241 return next({ ...e, props: { ...e.props, text: mask(e.props.text) } })
242 })
243
244 // Tool rows: the call's input is masked; a business tool's result is hidden whole
245 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
246 if (!rec.on) return next(e)
247 const business = businessSource(e.props.tool, e.props.input, config.businessTools) || businessCalls.has(e.props.tool_use_id)
248 if (business) businessCalls.add(e.props.tool_use_id)
249 const props = { ...e.props, input: deepMask(e.props.input, mask) }
250 if (e.props.output !== undefined) props.output = deepMask(e.props.output, business ? hideText : mask)
251 return next({ ...e, props })
252 })
253
254 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
255 if (!rec.on) return next(e)
256 const business = businessSource(e.props.tool, null, config.businessTools) || businessCalls.has(e.props.tool_use_id)
257 return next({ ...e, props: { ...e.props, output: deepMask(e.props.output, business ? hideText : mask) } })
258 })
259
260 // Just the indicator: a pulsing red dot and REC
261 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
262 const below = await next(e)
263 if (e.props && e.props.hasSurvey) return below
264 if (!rec.on) return below
265 const el = $.ui.resolve(e)
266 const line = e.surface === 'terminal' || !el.Svg
267 ? el.Text({ color: 'red', bold: true, children: ['● REC'] })
268 : el.Svg({ source: REC_SVG, alt: 'REC', width: 54, height: 18 })
269 return el.Box({ flexDirection: 'column', children: below ? [line, below] : [line] })
270 })
271
272 // The footer label shows even when the band is collapsed
273 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
274 if (!rec.on) return next(e)
275 const modes = Array.isArray(e.props && e.props.modes) ? e.props.modes : []
276 return next({ ...e, props: { ...e.props, modes: ['● REC', ...modes] } })
277 })
278}
279hooks/privacy.mjs 337 lines1// Masks secrets, personal details, and business figures for screen recording.
2// Pure functions: no mods API calls here. The same file ships in each mod that
3// draws text, since a mod may import only its own files.
4
5const DOTS = '••••••••'
6const HIDE = '•••'
7const HIDDEN_OUTPUT = '••• hidden while recording'
8
9const SECRET_PATTERNS = [
10 /sk-ant-[A-Za-z0-9_-]{16,}/g,
11 /\bsk-(?:proj-|live-|test-|svcacct-)?[A-Za-z0-9_-]{20,}/g,
12 /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
13 /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
14 /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
15 /\bAIza[0-9A-Za-z_-]{30,}/g,
16 /\bAKIA[0-9A-Z]{16}\b/g,
17 /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
18 /\b(?:hf|r8|pk_live|sk_live|rk_live|whsec|pat|key)_[A-Za-z0-9]{20,}/g,
19 /\bBearer\s+[A-Za-z0-9._~+/-]{20,}=*/gi,
20]
21
22// NAME=value or "name": "value" where the name looks like a credential
23const ASSIGNMENT =
24 /\b([A-Za-z0-9_.-]*(?:API[_-]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|CLIENT[_-]?SECRET|AUTH)[A-Za-z0-9_.-]*)(["']?\s*[=:]\s*["']?)([^\s"'`,;}{]{6,})/gi
25
26const EMAIL_SRC = '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}'
27const EMAIL = new RegExp(`\\b${EMAIL_SRC}\\b`, 'g')
28
29// People: "Jane Doe <jane@x.com>", "displayName": "Jane Doe", and From:/Attendees: lines
30const NAME_WORD = "[A-Z][\\p{L}'’.-]*"
31const DISPLAY_NAME = new RegExp(`(["']?)(${NAME_WORD}(?:[ \\t]+${NAME_WORD}){0,3})\\1([ \\t]*<[ \\t]*${EMAIL_SRC}[ \\t]*>)`, 'gu')
32const PERSON_KEY =
33 /("(?:displayName|display_name|fullName|full_name|firstName|first_name|lastName|last_name|givenName|given_name|familyName|family_name|real_name|realName|senderName|sender_name|authorName|author_name|username|user_name)"\s*:\s*")([^"]{1,80})(")/g
34const PERSON_LINE = /^([ \t>*-]*(?:From|To|Cc|Bcc|Reply-To|Attendees?|Organizer|Invitees?|Guests?|Assignees?|Sender|Owner)[ \t]*:[ \t]*)(\S.*)$/gm
35
36// Personal details
37const PHONE = /(?<![\w.])(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}(?![\w.])/g
38const PHONE_INTL = /(?<![\w.])\+\d{1,3}[\s.-]?\(?\d{1,4}\)?(?:[\s.-]?\d{2,4}){2,4}(?![\w.])/g
39const SSN = /(?<![\w-])\d{3}-\d{2}-\d{4}(?![\w-])/g
40const EIN = /(?<![\w-])\d{2}-\d{7}(?![\w-])/g
41const CARD = /(?<![\w-])(?:4\d{3}|5[1-5]\d{2}|2[2-7]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))(?:[ -]?\d{2,4}){3,4}(?![\w-])/g
42const IBAN = /\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]{4}){3,7}(?: ?[A-Z0-9]{1,3})?\b/g
43const ACCOUNT = /\b((?:routing|account|acct|a\/c|aba|swift|bic|iban|passport|driver'?s license)(?:\s*(?:number|no\.?|num|#))?\s*[:#]?\s*)(\d[\d -]{3,}\d)/gi
44const CARD_ENDING = /\b((?:ending(?:\s+in)?|last\s+(?:4|four)(?:\s+digits)?)\s*[:#]?\s*)(\d{4})\b/gi
45const STREET = /\b\d{1,6}\s+(?:[NSEW]\.?\s+)?(?:[A-Z][\p{L}'.-]*\s+){1,3}(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Place|Pl|Parkway|Pkwy|Highway|Hwy|Circle|Cir|Terrace|Ter|Trail|Trl|Square|Sq)\b\.?(?:,?\s+(?:Apt|Apartment|Suite|Ste|Unit|#)\.?\s*[\w-]+)?/gu
46const PO_BOX = /\bP\.?\s?O\.?\s+Box\s+\d+/gi
47const STATE_ZIP = /(,\s*(?:A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY]))\s+\d{5}(?:-\d{4})?\b/g
48const BIRTH = /\b((?:DOB|D\.O\.B\.|date of birth|birth\s?date|birthday|born(?: on)?)\s*[:-]?\s*)([A-Za-z0-9 ,/.-]{4,20}\d)/gi
49const IPV4 = /(?<![\w.])((?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3})(?![\w.])/g
50
51// Money in any common currency
52const MONEY = /(?:US|CA|AU)?[$€£¥]\s?\d[\d,]*(?:\.\d+)?(?:\s?(?:[kKmMbB]|bn|million|thousand|billion)\b)?/g
53const MONEY_WORD = /(?<![\w.])(?:(?:USD|EUR|GBP|CAD|AUD)\s?\d[\d,]*(?:\.\d+)?(?:\s?(?:[kKmMbB]|bn|million|thousand|billion)\b)?|\d[\d,]*(?:\.\d+)?(?:\s?(?:[kKmMbB]|bn|million|thousand|billion))?\s?(?:USD|EUR|GBP|CAD|AUD|dollars|bucks)\b)/gi
54
55// Figures near business words: "MRR is 84k", "40% margin", "profit share 20%"
56const FIN_WORD =
57 /\b(?:revenues?|mrr|arr|gmv|profits?|profit share|margins?|ebitda|income|earnings|payroll|salar(?:y|ies)|wages?|compensation|comp|bonus(?:es)?|equity|stakes?|ownership|valuation|runway|burn(?: rate)?|cash(?:flow)?|balances?|budgets?|spend(?:ing)?|pric(?:e|es|ing)|fees?|invoices?|payouts?|distributions?|dividends?|tax(?:es)?|sales|ltv|cac|aov|sponsor(?:s|ships?)?|cpm|rpm|retainers?|commissions?|royalt(?:y|ies)|refunds?|expenses?|debts?|loans?|funding|investments?|deals?|net|gross|paid|pays?|owed?|costs?)\b/gi
58const FIGURE = /(?<![\w.])\d[\d,]*(?:\.\d+)?(?:\s?(?:%|percent\b|[kKmMbB]\b|bn\b|million\b|thousand\b|billion\b|x\b))?/g
59const BIG_FIGURE = /(?<![\w.])(?:\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?\s?(?:%|percent\b|[kKmMbB]\b|bn\b|million\b|thousand\b|billion\b))/g
60
61// Single names that are also ordinary words, and names that stay visible (the channel's own)
62const COMMON_WORDS = new Set(
63 'Will Mark Grant Bill Rich Art Hope Faith Joy May June April August Summer Rose Page Chase Hunter Max Ray Dawn Sky Brook Lane Dean Gene Guy Frank Sterling Major Price Young King Long Little Wood Hill Stone Field Ford Hall Bell Rice Banks Case Cook Fox Gray Green Brown White Black Day Lee West North South Love Church Park Street Bishop Mason Miller Baker Carter Cole Wells Hart Moon Star Van Von De Del La Le Da Di St Mr Mrs Ms Dr Jr Sr Claude Code Team Support Admin Info Hello The And Ai Slack Gmail Google Calendar Meeting Sync Notes Update Review Weekly Daily Monday Tuesday Wednesday Thursday Friday Saturday Sunday'.split(' '),
64)
65const ROLE_LOCAL_PARTS = /^(?:info|hello|hi|team|support|help|admin|noreply|no-reply|donotreply|contact|sales|billing|accounts?|notifications?|news|newsletter|marketing|office|mail|security|privacy|legal|press|jobs|careers|hr|ops|dev|bot|alerts?|updates?|calendar|invites?)$/i
66
67// Files that stay closed while recording. Add your own folders and file names
68// in ~/.claude/mods-data/recording-mode/config.json ("privatePaths"); /rec config makes one.
69const PRIVATE_PATHS = [
70 /(^|[\\/\s"'`])\.env(\.[A-Za-z0-9_-]+)?(?=$|[\s"'`;|&)])/i,
71 /\.credentials\.json/i,
72 /[\\/]\.claude\.json\b/i,
73 /\.claude[\\/]projects[\\/][^\\/\s"'`]+[\\/]memory/i,
74 /(^|[\\/\s"'`])\.(?:ssh|aws|gnupg)(?=$|[\\/])/i,
75 /\bid_(?:rsa|ed25519|ecdsa)\b/i,
76 /(^|[\\/\s"'`])\.(?:netrc|npmrc|pypirc)\b/i,
77 /\.(?:pem|p12|pfx)\b/i,
78 /taxes?[-_ ]?20\d\d/i,
79 /[\\/_-](?:payroll|invoices?|contracts?|financials?|finances?|budgets?|forecasts?|cap[-_ ]?table|term[-_ ]?sheets?|offer[-_ ]?letters?|business[-_ ]?plans?)(?=$|[\\/._\s"'`;|&)-])/i,
80 /(^|[\s"'`])(?:payroll|invoices?|contracts?|financials?|finances?|budgets?|forecasts?|business[-_ ]?plans?)(?=[\\/.])/i,
81]
82
83// Tools whose results are business data: drawn as hidden while recording
84const BUSINESS_TOOL =
85 /(?:^|__|_)(?:clickup|slack|gmail|outlook|notion|asana|linear|jira|clay|qbo|quickbooks|xero|fireflies|hubspot|salesforce|stripe|calendar|gcal|google_drive|drive|sheets)|search_threads|get_thread|get_message|list_drafts|get_draft|read_file_content|download_file_content|search_files|list_recent_files|get_file_metadata|profit_loss|cash_flow|balance_sheet|payroll|session_transcripts|export_transcript/i
86const BUSINESS_COMMAND = /\bgws(?:\.cmd|\.exe)?\b|fireflies|quickbooks/i
87// Finance tools stay closed while recording
88const FINANCE_TOOL = /__(?:qbo_|quickbooks|profit_loss|cash_flow|balance_sheet|benchmarking_quickbooks|money_onboarding|company_info)/i
89
90function escapeRegExp(s) {
91 return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
92}
93
94// Values from .env files: long enough to be secret, and not plain words or numbers.
95export function secretValuesFromEnv(text) {
96 const values = []
97 for (const line of String(text || '').split(/\r?\n/)) {
98 const m = line.match(/^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.*)\s*$/)
99 if (!m) continue
100 let v = m[1].trim()
101 if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1)
102 if (v.length < 8) continue
103 if (/^(true|false|null|none|yes|no)$/i.test(v)) continue
104 if (/^\d+(\.\d+)?$/.test(v)) continue
105 if (/your[-_ ]?(key|token|secret)|here$|^<.*>$|^xxx/i.test(v)) continue
106 values.push(v)
107 }
108 return [...new Set(values)].sort((a, b) => b.length - a.length)
109}
110
111// The forms of one person's name that get masked: the full name, its slug, and each part that isn't an ordinary word
112function nameForms(full, keep) {
113 const clean = String(full || '').replace(/\s+/g, ' ').trim()
114 if (!clean || clean.length > 60 || keep.has(clean)) return []
115 const forms = new Set()
116 const parts = clean.split(/[ -]/).filter((p) => /^[A-Z][\p{L}'’.]*$/u.test(p) && p.length >= 2)
117 if (parts.length >= 2) {
118 forms.add(clean)
119 forms.add(clean.toLowerCase())
120 forms.add(clean.toLowerCase().replace(/ /g, '-'))
121 forms.add(clean.toLowerCase().replace(/ /g, '_'))
122 }
123 for (const p of parts) if (!COMMON_WORDS.has(p) && !keep.has(p)) forms.add(p)
124 return [...forms]
125}
126
127function namesFromEmail(email) {
128 const local = email.split('@')[0]
129 if (ROLE_LOCAL_PARTS.test(local)) return null
130 const parts = local.split(/[._+-]/).filter((p) => /^[a-z]{2,}$/i.test(p))
131 if (parts.length < 2 || parts.length > 4) return null
132 return parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(' ')
133}
134
135// Masks numbers in the same clause as a business word
136function maskFigures(text, strict) {
137 if (strict) text = text.replace(BIG_FIGURE, HIDE)
138 const ranges = []
139 for (const m of text.matchAll(FIN_WORD)) {
140 const s = m.index
141 const e = s + m[0].length
142 let a = Math.max(0, s - 30)
143 let b = Math.min(text.length, e + 45)
144 const before = text.slice(a, s)
145 const stop = Math.max(before.lastIndexOf('\n'), before.lastIndexOf(';'), before.search(/[.!?]\s[^.!?]*$/))
146 if (stop >= 0) a += stop + 1
147 const after = text.slice(e, b)
148 const end = after.search(/\n|;|[.!?](?:\s|$)/)
149 if (end >= 0) b = e + end
150 ranges.push([a, b])
151 }
152 if (!ranges.length) return text
153 return text.replace(FIGURE, (fig, offset) => {
154 if (/^(?:19|20)\d\d$/.test(fig)) return fig // a year
155 return ranges.some(([a, b]) => offset >= a && offset < b) ? HIDE : fig
156 })
157}
158
159function luhn(digits) {
160 let sum = 0
161 for (let i = 0; i < digits.length; i++) {
162 let d = Number(digits[digits.length - 1 - i])
163 if (i % 2 === 1) {
164 d *= 2
165 if (d > 9) d -= 9
166 }
167 sum += d
168 }
169 return sum % 10 === 0
170}
171
172function publicIp(ip) {
173 const [a, b] = ip.split('.').map(Number)
174 if (a === 10 || a === 127 || a === 0 || ip === '255.255.255.255') return false
175 if (a === 192 && b === 168) return false
176 if (a === 172 && b >= 16 && b <= 31) return false
177 if (a === 169 && b === 254) return false
178 return true
179}
180
181// Your own name and its parts stay visible
182function keepSet(keepNames) {
183 const keep = new Set()
184 for (const n of keepNames || []) {
185 const clean = String(n || '').replace(/\s+/g, ' ').trim()
186 if (!clean) continue
187 keep.add(clean)
188 keep.add(clean.toLowerCase())
189 keep.add(clean.toLowerCase().replace(/ /g, '-'))
190 for (const part of clean.split(/[ -]/)) if (part) keep.add(part)
191 }
192 return keep
193}
194
195// strict: also every large figure (1,250 / 18% / 40k) wherever it appears
196// names: people to mask; keepNames: names that stay visible (yours)
197export function makeMasker({ strict = false, values = [], names = [], keepNames = [] } = {}) {
198 const KEEP_NAMES = keepSet(keepNames)
199 const valueRe = values.length
200 ? new RegExp(values.map(escapeRegExp).join('|'), 'g')
201 : null
202 // Names: the roster passed in plus any learned from emails and contact fields. Memory only.
203 const people = new Set()
204 let nameRe = null
205 let dirty = false
206 const learn = (full) => {
207 if (people.size > 2000) return
208 for (const f of nameForms(full, KEEP_NAMES)) {
209 if (!people.has(f)) {
210 people.add(f)
211 dirty = true
212 }
213 }
214 }
215 for (const n of names) learn(n)
216 const nameRegex = () => {
217 if (dirty) {
218 const list = [...people].sort((a, b) => b.length - a.length).map(escapeRegExp)
219 nameRe = list.length ? new RegExp(`(?<![\\p{L}\\p{N}_])(?:${list.join('|')})(?:['’]s)?(?![\\p{L}\\p{N}_])`, 'gu') : null
220 dirty = false
221 }
222 return nameRe
223 }
224
225 return function mask(text) {
226 if (typeof text !== 'string' || text.length === 0) return text
227 let out = text
228 if (valueRe) out = out.replace(valueRe, DOTS)
229 for (const re of SECRET_PATTERNS) out = out.replace(re, DOTS)
230 out = out.replace(ASSIGNMENT, (_, name, sep) => name + sep + DOTS)
231
232 // people, learned before the emails that name them are masked
233 for (const m of out.matchAll(EMAIL)) {
234 const n = namesFromEmail(m[0])
235 if (n) learn(n)
236 }
237 out = out.replace(DISPLAY_NAME, (_, q, name, addr) => {
238 learn(name)
239 return KEEP_NAMES.has(name) ? q + name + q + addr : HIDE + addr
240 })
241 out = out.replace(PERSON_KEY, (_, head, value, tail) => {
242 learn(value)
243 return head + (KEEP_NAMES.has(value) ? value : HIDE) + tail
244 })
245 out = out.replace(PERSON_LINE, (line, label, value) => (KEEP_NAMES.has(value.trim()) ? line : label + HIDE))
246 out = out.replace(EMAIL, '•••@•••')
247 const re = nameRegex()
248 if (re) out = out.replace(re, HIDE)
249
250 // personal details
251 out = out.replace(CARD, (m) => {
252 const digits = m.replace(/\D/g, '')
253 return digits.length >= 13 && digits.length <= 19 && luhn(digits) ? '•••• •••• •••• ••••' : m
254 })
255 out = out.replace(SSN, '•••-••-••••')
256 out = out.replace(EIN, '••-•••••••')
257 out = out.replace(IBAN, HIDE)
258 out = out.replace(ACCOUNT, (_, head) => head + HIDE)
259 out = out.replace(CARD_ENDING, (_, head) => head + '••••')
260 out = out.replace(BIRTH, (_, head) => head + HIDE)
261 out = out.replace(STREET, HIDE)
262 out = out.replace(PO_BOX, HIDE)
263 out = out.replace(STATE_ZIP, (_, head) => head + ' •••••')
264 out = out.replace(PHONE, '•••-•••-••••')
265 out = out.replace(PHONE_INTL, '+•• •••')
266 out = out.replace(IPV4, (ip) => (publicIp(ip) ? '•••.•••.•••.•••' : ip))
267
268 // business figures
269 out = out.replace(MONEY, '$•••')
270 out = out.replace(MONEY_WORD, HIDE)
271 out = maskFigures(out, strict)
272 return out
273 }
274}
275
276// Masks every string inside plain data, keeping the shape.
277export function deepMask(value, mask, depth = 0) {
278 if (depth > 12) return value
279 if (typeof value === 'string') return mask(value)
280 if (Array.isArray(value)) return value.map((v) => deepMask(v, mask, depth + 1))
281 if (value && typeof value === 'object') {
282 const proto = Object.getPrototypeOf(value)
283 if (proto !== Object.prototype && proto !== null) return value
284 const out = {}
285 for (const [k, v] of Object.entries(value)) out[k] = deepMask(v, mask, depth + 1)
286 return out
287 }
288 return value
289}
290
291// Hides the readable text inside a business tool's result, keeping short tags (type: 'text') so the row still draws.
292export function hideText(s) {
293 if (typeof s !== 'string' || s.length === 0) return s
294 return s.length > 24 || /\s/.test(s) ? HIDDEN_OUTPUT : s
295}
296
297// extra: your own folder or file names from the config file, matched anywhere in a path, any case
298export function privatePathHit(text, extra = []) {
299 const s = String(text || '')
300 for (const re of PRIVATE_PATHS) {
301 const m = s.match(re)
302 if (m) return m[0].trim().replace(/^["'`\\/_-]/, '')
303 }
304 const flat = s.replace(/\\/g, '/').toLowerCase()
305 for (const item of extra) {
306 const needle = String(item || '').replace(/\\/g, '/').toLowerCase().trim()
307 if (needle && flat.includes(needle)) return String(item)
308 }
309 return null
310}
311
312// The strings in a tool call's arguments that could name a file or carry a command.
313export function toolCallTargets(e) {
314 const keys = ['file_path', 'path', 'pattern', 'glob', 'command', 'notebook_path', 'url']
315 const out = []
316 for (const k of keys) if (typeof e?.[k] === 'string') out.push(e[k])
317 return out
318}
319
320// Whether a tool's result is business data (mail, chat, tasks, files, calendar, finance)
321// extra: more tool or command names (any part of the name) whose results draw as hidden
322export function businessSource(tool, input, extra = []) {
323 const name = String(tool || '')
324 if (BUSINESS_TOOL.test(name)) return true
325 const cmd = input && typeof input.command === 'string' ? input.command : ''
326 const hay = (name + ' ' + cmd).toLowerCase()
327 if (extra.some((x) => x && hay.includes(String(x).toLowerCase()))) return true
328 const command = input && typeof input.command === 'string' ? input.command : ''
329 return command ? BUSINESS_COMMAND.test(command) : false
330}
331
332// extra: more tool names (any part of the name) that stay closed while recording
333export function financeTool(tool, extra = []) {
334 const name = String(tool || '')
335 return FINANCE_TOOL.test(name) || extra.some((x) => x && name.toLowerCase().includes(String(x).toLowerCase()))
336}
337