SLOPSHOPPER

record-mode

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

newspinnerrowsguardcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · record-mode
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ record-mode │ ⏺ Read(src/auth.ts) │ ● REC on: personal info is masked on │ ⎿ Read 6 lines │ screen │ ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /record ⎿ record-mode: Recording mode ON. Emails, phone numbers and money are masked on screen. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

record-mode

/record on before you start a screen recording, /record off after. While it's on, the transcript draws these as ▇▇▇ masks:

  • email addresses
  • phone numbers
  • dollar amounts
  • your extra terms
  • Korean text, if you turn that on

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.

Settings

SettingDefaultWhat it is
extra_termsemptyComma-separated words, names or domains to mask too. Case-insensitive.
mask_koreanoffon also masks Korean text.
Source 2 files
hooks/register.tsx 86 lines
1// 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}
86
types/index.d.ts 8 lines
1export type RecordMode = { isOn: boolean }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'record-mode': { isOn: boolean }
6  }
7}
8