While you record your screen, /record on masks emails, phone numbers, money and your own private terms in the transcript

/record on before you start a screen recording, /record off after. While it's on, the transcript draws these as ▇▇▇ masks:
A red "● REC" shows in the prompt footer so you know it's on. It stays on across sessions until you turn it off. Claude can also turn it on or off for you.
It's display only. Claude still reads the real text, and nothing in your files or messages changes. Check a test recording before you rely on it: anything drawn outside the transcript (tool output panes, other mods) isn't masked.
| Setting | Default | What it is |
|---|---|---|
extra_terms | empty | Comma-separated words, names or domains to mask too. Case-insensitive. |
mask_korean | off | on also masks Korean text. |
hooks/register.tsx 86 lines1// record-mode: /record on|off. While on, the transcript draws personal details as ▇▇▇ masks:
2// emails, phone numbers, dollar amounts, your extra terms and (optionally) Korean text.
3// Display only: Claude still reads the real text. Adds a red "● REC" label to the prompt footer.
4import { atom, read, update } from 'claude-code'
5import type { Register } from 'claude-code'
6
7const isOn = atom({ plugin: 'record-mode', key: 'isOn' } as const, false)
8
9type Rule = [RegExp, (m: string) => string]
10const BASE: Rule[] = [
11 [/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, () => '▇▇▇@▇▇▇'],
12 [/(?:\+?\d{1,2}[\s.-]?)?\(?\b\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b/g, () => '▇▇▇-▇▇▇-▇▇▇▇'],
13 [/\$\s?\d[\d,]*(\.\d+)?(?:\s?[kKM]\b)?/g, () => '$▇▇'],
14]
15const KOREAN: Rule = [/[가-힣ㄱ-ㆎ]+(?:\s+[가-힣ㄱ-ㆎ]+)*/g, m => '▇'.repeat(Math.min(8, Math.max(2, Math.ceil(m.length / 2))))]
16let rules: Rule[] = BASE
17let what = 'Emails, phone numbers and money'
18
19export const mask = (text: string) => {
20 let n = 0
21 let out = text
22 for (const [re, f] of rules) out = out.replace(re, m => (n++, f(m)))
23 return { out, n }
24}
25
26export const register: Register = (on, options) => {
27 const terms = String(options.extra_terms ?? '').split(',').map(t => t.trim()).filter(Boolean)
28 const korean = options.mask_korean === 'on'
29 rules = [
30 ...BASE,
31 ...terms.map((t): Rule => [new RegExp(t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), m => '▇'.repeat(Math.min(8, Math.max(3, m.length)))]),
32 ...(korean ? [KOREAN] : []),
33 ]
34 const extras = [terms.length ? 'your extra terms' : '', korean ? 'Korean text' : ''].filter(Boolean)
35 what = extras.length ? `Emails, phone numbers, money, ${extras.join(' and ')}` : 'Emails, phone numbers and money'
36
37 on('session.start', async ($, e, next) => {
38 await $.tool.register({ name: 'toggle', description: 'Turn recording mode on or off', inputSchema: { type: 'object', properties: { on: { type: 'boolean' } }, required: ['on'] } })
39 await $.command.register({ name: 'record', description: 'Recording mode: /record on | off (masks personal info on screen)' })
40 const saved = (await $.store.get('isOn')) === true
41 await update($, isOn, () => saved)
42 return next(e)
43 })
44
45 on('command.run', { command: 'record' }, async ($, e) => {
46 const arg = e.args.trim().toLowerCase()
47 const want = arg === 'off' ? false : arg === 'on' ? true : !(await read($, isOn))
48 await update($, isOn, () => want)
49 await $.store.set('isOn', want)
50 $.ui.invalidate('ui.render')
51 $.ui.toast(want ? '● REC on: personal info is masked on screen' : '■ REC off: everything is visible again')
52 return { text: want ? `Recording mode ON. ${what} are masked on screen.` : 'Recording mode OFF.' }
53 })
54
55 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
56 if (!(await read($, isOn))) return next(e)
57 const { out, n } = mask(e.props.text)
58 return n ? next({ ...e, props: { ...e.props, text: out } }) : next(e)
59 })
60
61 on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
62 if (!(await read($, isOn))) return next(e)
63 const { out, n } = mask(e.props.text)
64 return n ? next({ ...e, props: { ...e.props, text: out } }) : next(e)
65 })
66
67 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
68 if (!(await read($, isOn))) return next(e)
69 return next({ ...e, props: { ...e.props, modes: ['● REC', ...e.props.modes] } })
70 })
71
72 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
73 if (!(await read($, isOn))) return next(e)
74 return next({ ...e, props: { ...e.props, tail: '● recording: personal info masked' } })
75 })
76
77 on('tool.call', { tool: 'mcp__record-mode__toggle' }, async ($, e) => {
78 const want = (e as any).on === true
79 await update($, isOn, () => want)
80 await $.store.set('isOn', want)
81 $.ui.invalidate('ui.render')
82 $.ui.toast(want ? '● REC on: personal info is masked on screen' : '■ REC off: everything is visible again')
83 return { result: (want ? 'Recording mode on.' : 'Recording mode off.') }
84 })
85}
86types/index.d.ts 8 lines1export type RecordMode = { isOn: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'record-mode': { isOn: boolean }
6 }
7}
8