For screen sharing and recordings: hides API keys, tokens, private keys, e-mail addresses, IP addresses, your login name in paths and any names you list. Only…

For screen sharing and recordings: hides API keys, tokens, private keys, e-mail addresses, IP addresses, your login name in paths and any names you list. Only the display changes; Claude still reads the original. Nothing leaves the machine. /mask switches it.
Part of the Mod Box, which bundles it with other mods and one window to switch and configure them all. This is the same mod on its own.
In a terminal Claude Code session (2.1.287 or newer):
/plugin install screen-mask --marketplace LupusMalusDeviant/claude-mods
| Command | What it does | |
|---|---|---|
| `/mask [on\ | off]` | hide secrets and names on screen |
Set them in /config (or when installing).
| Option | Default | What it does |
|---|---|---|
terms | `` | Comma separated, e.g. customer names, project code names, a server name. Case does not matter. |
emails | on | Every address becomes [e-mail]. |
ips | on | IPv4 addresses except loopback and documentation ranges become [ip]. |
language | en | Language of the texts this mod shows and writes. |
MIT, see LICENSE.
hooks/entry.tsx 30 lines1// Entry of the standalone Screen Mask. The mod hands its display to a band (in the Mod Box the
2// band manager draws it); here this hook, registered first and so the outermost, draws it below
3// whatever else is above the prompt.
4import type { Register } from 'claude-code'
5
6import { register as inner } from '../mods/screen-mask/hooks/register'
7import { openBand, takeBand } from '../mods/settings/hooks/band'
8
9export const register: Register = (on, options) => {
10 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
11 openBand(e)
12 const rest = await next(e)
13 const parts = takeBand(e)
14 if (e.props.hasSurvey || parts.length === 0) return rest
15 const { Box, Text } = $.ui.resolve(e)
16 return (
17 <Box flexDirection="column">
18 {rest}
19 {parts.map(p => (
20 <Box key={`band-${p.id}`} flexDirection="row" paddingX={1}>
21 <Box flexShrink={1}>{p.line as any}</Box>
22 {(p.actions as any[]).flatMap((a, i) => [<Text key={`gap-${i}`}>{' '}</Text>, a])}
23 </Box>
24 ))}
25 </Box>
26 )
27 })
28 ;(inner as any)(on, options)
29}
30mods/screen-mask/hooks/register.tsx 105 lines1// Screen Mask: while you share your screen, the transcript hides API keys, tokens, private keys,
2// e-mail addresses, IP addresses, your login name in paths and any names you list. Only the
3// drawing changes: Claude still reads and writes the original text. Nothing leaves the machine.
4// /mask switches it; the band shows when it is on.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8import { offer as offerBand } from '../../settings/hooks/band'
9import type { BandPart } from '../../settings/hooks/band'
10import { type MaskRules, maskDeep, maskText, parseTerms } from './mask'
11
12const isOn = atom({ plugin: 'screen-mask', key: 'on' } as const, false)
13// Kept across sessions: a restart in the middle of a screen share must not uncover anything.
14const STORE_ON = 'sm:on'
15
16let lang: 'de' | 'en' = 'en'
17const t = (de: string, en: string) => (lang === 'en' ? en : de)
18const rules: MaskRules = { terms: [], user: '', emails: true, ips: true }
19
20const mask = (s: string) => maskText(s, rules)
21
22async function setOn($: EngineInterface, on: boolean) {
23 await update($, isOn, () => on)
24 await $.store.set(STORE_ON, on).catch(() => {})
25 $.ui.toast(on ? t('🙈 Bildschirmschutz an: Geheimnisse und Namen sind in der Anzeige verdeckt.', '🙈 Screen mask on: secrets and names are hidden on screen.') : t('Bildschirmschutz aus.', 'Screen mask off.'))
26}
27
28export const register: Register = (on, options) => {
29 lang = options.language === 'de' ? 'de' : 'en'
30 rules.terms = parseTerms(String(options.terms ?? ''))
31 rules.emails = String(options.emails ?? 'on') !== 'off'
32 rules.ips = String(options.ips ?? 'on') !== 'off'
33
34 on('session.start', {}, async ($, e, next) => {
35 const r = await next(e)
36 rules.user = String((await $.env.get('USERNAME')) || (await $.env.get('USER')) || '')
37 await $.command.register({ name: 'mask', description: t('Bildschirmschutz: Geheimnisse und Namen in der Anzeige verdecken', 'Screen mask: hide secrets and names on screen'), argumentHint: '[on|off]' })
38 if ((await $.store.get(STORE_ON).catch(() => false)) === true) await update($, isOn, () => true)
39 return r
40 })
41
42 on('command.run', { command: 'mask' }, async ($, e) => {
43 const arg = String(e.args ?? '').trim().toLowerCase()
44 const want = arg === 'on' || arg === 'an' ? true : arg === 'off' || arg === 'aus' ? false : !(await read($, isOn))
45 await setOn($, want)
46 return { text: want ? t('Bildschirmschutz an.', 'Screen mask on.') : t('Bildschirmschutz aus.', 'Screen mask off.') }
47 })
48
49 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
50 if (!(await read($, isOn))) return next(e)
51 return next({ ...e, props: { ...e.props, text: mask(e.props.text) } })
52 })
53
54 on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
55 if (!(await read($, isOn))) return next(e)
56 return next({ ...e, props: { ...e.props, text: mask(e.props.text) } })
57 })
58
59 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
60 if (!(await read($, isOn))) return next(e)
61 const props = e.props as Record<string, unknown>
62 return next({ ...e, props: { ...e.props, input: maskDeep(props.input, mask), ...(props.output === undefined ? {} : { output: maskDeep(props.output, mask) }) } } as typeof e)
63 })
64
65 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
66 if (!(await read($, isOn))) return next(e)
67 const props = e.props as Record<string, unknown>
68 return next({ ...e, props: { ...e.props, output: maskDeep(props.output, mask) } } as typeof e)
69 })
70
71 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
72 if (!(await read($, isOn))) return next(e)
73 return next({ ...e, props: { ...e.props, calls: e.props.calls.map(c => ({ ...c, input: maskDeep((c as Record<string, unknown>).input, mask) })) } } as typeof e)
74 })
75
76 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
77 const rest = await next(e)
78 if (!e.props.hasSurvey) {
79 const part = await band($, e).catch(() => null)
80 if (part) offerBand(e, part)
81 }
82 return rest
83 })
84}
85
86export async function band($: EngineInterface, e: any): Promise<BandPart | null> {
87 if (!(await read($, isOn))) return null
88 const { Text, Button } = $.ui.resolve(e)
89 const what = [t('Geheimnisse', 'secrets'), rules.emails ? t('E-Mails', 'e-mails') : '', rules.ips ? 'IPs' : '', rules.user ? t('Benutzername', 'login name') : '', rules.terms.length ? t(`${rules.terms.length} Begriffe`, `${rules.terms.length} terms`) : ''].filter(Boolean).join(', ')
90 return {
91 id: 'screenMask',
92 urgent: false,
93 short: <Text color="yellow">🙈</Text>,
94 line: (
95 <Text>
96 <Text color="yellow" bold>{'🙈 '}</Text>
97 <Text>{t('Bildschirmschutz an: ', 'Screen mask on: ')}</Text>
98 <Text dimColor>{what}</Text>
99 </Text>
100 ),
101 actions: [<Button key="sm-off" label={t('Aus', 'Off')} plain onPress={() => void setOn($, false)} />],
102 status: `🙈 ${t('Bildschirmschutz', 'Screen mask')}`,
103 }
104}
105mods/settings/hooks/band.ts 44 lines1// The band manager's mailbox. A band mod computes its display in its own AbovePrompt hook (with its
2// own $; the validator does not let $ cross an import) and drops it here with offer(). The settings
3// hook, registered first and so the outermost, runs after all of them, takes the parts and draws
4// them ordered, compact or one line each, or sends them to the status line.
5export type BandPart = {
6 /** The mod's switch key, e.g. "cacheMeter". */
7 id: string
8 /** Wants attention (CI red, sign offer …): always its own line with its buttons. */
9 urgent: boolean
10 /** One short element for the compact line. */
11 short: unknown
12 /** The full element for its own line. */
13 line: unknown
14 /** Buttons that belong to the part. */
15 actions: unknown[]
16 /** Plain text for the status line. */
17 status: string
18}
19
20const parts = new Map<string, BandPart[]>()
21
22function keyOf(e: { requestId?: string }): string {
23 return e.requestId ?? 'band'
24}
25
26/** Called by the manager before the inner hooks run. */
27export function openBand(e: { requestId?: string }) {
28 parts.set(keyOf(e), [])
29}
30
31/** Called by a band mod with its display for this render. */
32export function offer(e: { requestId?: string }, part: BandPart) {
33 const list = parts.get(keyOf(e))
34 if (list) list.push(part)
35 else parts.set(keyOf(e), [part])
36}
37
38/** Called by the manager after the inner hooks ran. */
39export function takeBand(e: { requestId?: string }): BandPart[] {
40 const list = parts.get(keyOf(e)) ?? []
41 parts.delete(keyOf(e))
42 return list
43}
44mods/screen-mask/hooks/mask.ts 57 lines1// Screen Mask: what to hide while the screen is shared, and how. No $ here.
2import { PATTERNS, isPlaceholder } from '../../secret-guard/hooks/patterns'
3
4export type MaskRules = {
5 /** Words or names to hide wherever they appear (case-insensitive). */
6 terms: string[]
7 /** The person's login name, hidden in paths like C:\Users\<name> or /home/<name>. */
8 user: string
9 emails: boolean
10 ips: boolean
11}
12
13const EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g
14// IPv4 outside the loopback and documentation ranges.
15const IPV4 = /\b(?!127\.|0\.0\.0\.0\b|192\.0\.2\.|198\.51\.100\.|203\.0\.113\.)(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}\b/g
16const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
17
18/** The text with every secret, address and listed term replaced by a short label. */
19export function maskText(text: string, rules: MaskRules): string {
20 if (!text) return text
21 let out = text
22 for (const [what, re] of PATTERNS) {
23 out = out.replace(new RegExp(re.source, re.flags), m => (isPlaceholder(m) ? m : `[${what}]`))
24 }
25 if (rules.emails) out = out.replace(EMAIL, '[e-mail]')
26 if (rules.ips) out = out.replace(IPV4, '[ip]')
27 if (rules.user.length >= 2) {
28 const u = escape(rules.user)
29 out = out.replace(new RegExp(`([\\\\/](?:Users|home)[\\\\/])${u}(?=[\\\\/]|\\b)`, 'gi'), '$1•••')
30 }
31 for (const term of rules.terms) {
32 if (term.length < 2) continue
33 out = out.replace(new RegExp(`(?<![\\w])${escape(term)}(?![\\w])`, 'gi'), '•••')
34 }
35 return out
36}
37
38/** Every string inside a value (tool inputs and outputs are objects), masked. */
39export function maskDeep<T>(value: T, mask: (s: string) => string, depth = 0): T {
40 if (depth > 12) return value
41 if (typeof value === 'string') return mask(value) as T
42 if (Array.isArray(value)) return value.map(v => maskDeep(v, mask, depth + 1)) as T
43 if (value && typeof value === 'object') {
44 const out: Record<string, unknown> = {}
45 for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = maskDeep(v, mask, depth + 1)
46 return out as T
47 }
48 return value
49}
50
51/** The terms option: comma or newline separated. */
52export const parseTerms = (raw: string) =>
53 raw
54 .split(/[,\n]/)
55 .map(s => s.trim())
56 .filter(s => s.length >= 2)
57mods/secret-guard/hooks/patterns.ts 27 lines1// The patterns of known secrets, on their own: Screen Mask uses them too, and a standalone
2// export of it then carries only these, not Secret Guard's hooks and state.
3
4// Patterns of known secrets. Kept narrow, so examples and placeholders pass.
5export const PATTERNS: Array<[string, RegExp]> = [
6 ['Anthropic-API-Key', /sk-ant-[A-Za-z0-9_-]{20,}/g],
7 ['OpenAI-API-Key', /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_-]{32,}/g],
8 ['GitHub-Token', /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})/g],
9 ['GitLab-Token', /\bglpat-[A-Za-z0-9_-]{20,}/g],
10 ['AWS-Zugangsschlüssel', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g],
11 ['Google-API-Key', /\bAIza[0-9A-Za-z_-]{35}\b/g],
12 ['Slack-Token', /\bxox[abprs]-[A-Za-z0-9-]{10,}/g],
13 ['Stripe-Live-Key', /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g],
14 ['Hugging-Face-Token', /\bhf_[A-Za-z0-9]{30,}/g],
15 ['privater Schlüssel', /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY(?: BLOCK)?-----/g],
16 ['JWT', /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g],
17 [
18 'credentials in an assignment',
19 /\b(?:api[_-]?key|secret|token|passw(?:or)?d|client[_-]?secret|access[_-]?key)\b["']?\s*[:=]\s*["'](?![^"']*(?:example|beispiel|changeme|placeholder|your[_-]|xxx|<|\$\{|\{\{))[A-Za-z0-9+/_\-.=]{20,}["']/gi,
20 ],
21]
22
23/** Placeholders and documentation examples: they look like keys but are none. */
24export function isPlaceholder(value: string): boolean {
25 return /example|dummy|fake|placeholder|redacted|sample|x{6,}|0{10,}|\*{4,}|your[_-]?(?:api|key|token|secret)/i.test(value)
26}
27mods/screen-mask/types/contract.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'screen-mask': { on: boolean }
4 }
5}
6