SLOPSHOPPER

screen-share

投屏/录屏隐私打码:/screen share 一键把邮箱、电话、金额、密钥、人名公司名遮成灰色条(只改画面,不改真实数据)

newspinnerrowscommandstatustimer
v0.2.0no licenseupdated 2026-10-23weizhongl/claude-code-mods/screen-share
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · screen-share
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ 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 › /screen ⎿ screen-share: 屏幕共享模式已开启:邮箱、电话、金额、密钥、人名公司名已遮成灰条。再输入 /screen share 关闭。 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ screen-share: 👁 屏幕共享:打码中
README

Claude Code Mods

十个给 Claude Code 用的 Mod。只能装在 Claude Code(终端和桌面 App 的 Code 页)。Codex、Anti-Gravity 没有 Mod 接口,装不了。

先装 switchboard(总控台),其余随意挑。

安装

终端里:

/plugin install switchboard --marketplace weizhongl/claude-code-mods

第一次会问 Add marketplace?,回 y,再选 user scope。其余 Mod 把名字换掉即可:usage-gauge、brain-dump、helper-desk、cache-clock、handoff、reflect、chat-inventory、screen-share、replay-theater。

在 shell 里:

claude plugin install switchboard --marketplace weizhongl/claude-code-mods

桌面 App(/plugin 在那里不能用):点输入框旁的 +,选 Plugins → Add marketplace,填 weizhongl/claude-code-mods,再逐个 Add plugin。

每个 Mod 怎么用

Mod怎么用注意
switchboard输入 /mods 打开总控台。每个 Mod 一行:On/Off、Hide UI Panel。输入框上方那一行条(额度、缓存倒计时、任务进度、👁 按钮)也是它画的:平时只占一行,别的 Mod 有事才在它旁边多出一行。条的右端有按钮:「收起」变成一小行(只剩几个百分比和倒计时),再点「隐藏」只留一个「📊 展开」按钮;也可以输入 /bar(或 /bar full、/bar mini、/bar off)切换,你的选择会记住。进度条在桌面版是细线,在终端里是小方块。也可以 /mods off 名字、/mods on 名字、/mods hide 名字、/mods show 名字。新放进同一个文件夹的 Mod 会自动出现。Off = 该 Mod 的钩子立刻停手;Hide = 后台照跑,只收起面板。
usage-gauge自动。显示在总控台那一行条里:对话上下文用了多少、5 小时(会话)额度、本周额度,各有短进度条、颜色(绿 / 黄 / 红)和重置时间,最后是本次对话花费。/gauge 看文字版。读的是引擎自己的数字(和状态栏同一份),零 Token。额度数字要等发过一条消息、引擎读到后才出现。
brain-dump直接打字或语音说一大段(默认 180 字以上、没有排版)。它会拦住,先问你 2–3 个问题;你在输入框直接回答(回「跳过」= 不补充),然后重写成 The Task / The Why / Guardrails / What Done Looks Like 再发给 Claude。输入框上方有 跳过提问 / 原话直接发 / 放弃 三个按钮,也可用 /dump skip、/dump raw、/dump cancel。消息开头加 >> 可以跳过拦截。会用到两次小的模型调用(提问用 haiku,重写用 sonnet)。
helper-desk自动。Claude 有待办清单时,一行条里显示「任务 n/m」和短进度条;Claude 派出子代理时,侧边栏出现 Helper Desk,每个子代理一张卡(进度、正在做什么、读入/输出 token、花费)。/helpers 手动打开。子代理花费是按公开标价估算;整段对话总花费取自引擎账本。零额外 Token。
cache-clock自动。第一条回复之后,一行条里显示「⏱ mm:ss」。点旁边的 Keep Warm 或输入 /keep warm,快冷之前会自动发一条极小的心跳续命。你发下一条真实消息,或连续保温满 2 小时,会自动关闭。拿不到服务器的缓存时间,所以倒计时是推算(订阅登入 60 分钟,API 5 分钟,并按真实缓存命中自动校正)。设置里的「缓存有效分钟数」可固定。
handoff对话累计到 250,000 token、或缓存变冷时,输入框上方提醒你。按「生成交接」或输入 /handoff,会写出一份详细的交接提示词(存在 ~/.claude/handoffs/,可直接发给同事),再按 Start Fresh Chat with Handoff 带去新对话。生成会花 Token,默认只提醒、不自动生成(设置里「触发后做什么」选 generate 才会自动生成)。桌面 App 不允许自动开新对话时,会改成复制到剪贴板。
reflect你在对话里说「以后永远不要…」「每次都要…」「记住…」这类话,输入框上方弹出小卡片,一键存进:内置 Memory / 项目 CLAUDE.md(没有就 AGENTS.md)/ 你指定的文件 / 仅本次忽略。只在句子里有规则关键词时才调用一次小模型判断。设置里可填默认的第二大脑文件路径。
chat-inventory自动。侧边栏「In this chat」:目标与待办、调用过的连接器(含次数)、用过的技能、读过和新建/修改过的文件(点文件在文件管理器里定位)。/inventory 手动打开。纯追踪,零 Token。
screen-share输入 /screen share 开或关,或点一行条右端的 👁 按钮。开启后,画面上的邮箱、电话、金额、API key、公司名、人名遮成灰条。只改画面,不改真实数据;不遮输入框里正在打的字。人名只遮设置里填的名单和「客户:xxx」「陈小姐」这类写法,没有真正的人名识别。
replay-theater自动录下 Claude 用 Edit / Write / NotebookEdit 对文件做的每次修改。输入 /replay 打开:选文件、播放/暂停、上一步/下一步、点步骤号跳转,看每一步的 diff。Revert 按钮按一次变成「再按一次确认」,第二次才会写回文件。/replay clear 清空。用 Bash 改文件(如 sed)录不到;单个文件超过 20 万字不录。记录只在当前会话。

目录

每个 Mod 一个文件夹:.claude-plugin/plugin.json、hooks/hooks.json、hooks/register.tsx,有状态的还有 types/index.d.ts。switchboard、screen-share、replay-theater 带自动测试(claude plugin test <文件夹>)。

各 Mod 里的 hooks/flag.ts 是同一小段代码(读总控台的开关),Mod 之间不能共享文件,所以每个放一份。

Source 4 files
hooks/register.tsx 93 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { flagOf } from './flag'
5import { maskDeep, namesPattern } from './mask'
6
7const MOD = 'screen-share'
8const isOn = atom({ plugin: 'screen-share', key: 'isOn' } as const, false)
9
10// The strip's eye button bumps this counter; only this mod may flip its own switch.
11const TOGGLES = { plugin: 'switchboard', key: 'toggles' } as const
12let seenToggle = -1
13
14async function setShare($: EngineInterface, force?: boolean): Promise<boolean> {
15  const wanted = force ?? !(await read($, isOn))
16  await update($, isOn, () => wanted)
17  $.ui.status(wanted ? '👁 屏幕共享:打码中' : undefined)
18
19  return wanted
20}
21
22async function pollToggle($: EngineInterface) {
23  const pressed = ((await $.state.get(TOGGLES)).value ?? {})['screen-share'] ?? 0
24  if (seenToggle < 0) seenToggle = pressed
25  if (pressed !== seenToggle) {
26    seenToggle = pressed
27    if ((await flagOf($, MOD)).isOn) await setShare($)
28  }
29}
30
31async function isMasking($: EngineInterface): Promise<boolean> {
32  const flag = await flagOf($, MOD)
33
34  return (await read($, isOn)) && flag.isOn
35}
36
37export const register: Register = (on, options) => {
38  const names = namesPattern(String(options.names ?? ''))
39
40  on('session.start', async ($, e, next) => {
41    await $.command.register({
42      name: 'screen',
43      description: '屏幕共享打码模式:/screen share 开 / 关',
44      argumentHint: 'share',
45    })
46    $.clock.every(500, () => void pollToggle($).catch(() => undefined))
47
48    return next(e)
49  })
50
51  on('command.run', { command: 'screen' }, async $ => {
52    const wanted = await setShare($)
53
54    return {
55      text: wanted
56        ? '屏幕共享模式已开启:邮箱、电话、金额、密钥、人名公司名已遮成灰条。再输入 /screen share 关闭。'
57        : '屏幕共享模式已关闭,恢复明文。',
58    }
59  })
60
61  // The transcript rows keep their real text; only what is drawn is rewritten.
62  on('ui.render', { component: 'UserMessage' }, async ($, e, next) =>
63    (await isMasking($))
64      ? next({ ...e, props: maskDeep(e.props, names) } as typeof e)
65      : next(e),
66  )
67  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) =>
68    (await isMasking($))
69      ? next({ ...e, props: maskDeep(e.props, names) } as typeof e)
70      : next(e),
71  )
72  on('ui.render', { component: 'ToolUse' }, async ($, e, next) =>
73    (await isMasking($))
74      ? next({ ...e, props: maskDeep(e.props, names) } as typeof e)
75      : next(e),
76  )
77  on('ui.render', { component: 'ToolResult' }, async ($, e, next) =>
78    (await isMasking($))
79      ? next({ ...e, props: maskDeep(e.props, names) } as typeof e)
80      : next(e),
81  )
82  on('ui.render', { component: 'CommandOutput' }, async ($, e, next) =>
83    (await isMasking($))
84      ? next({ ...e, props: maskDeep(e.props, names) } as typeof e)
85      : next(e),
86  )
87  on('ui.render', { component: 'InfoNotice' }, async ($, e, next) =>
88    (await isMasking($))
89      ? next({ ...e, props: maskDeep(e.props, names) } as typeof e)
90      : next(e),
91  )
92}
93
hooks/flag.ts 14 lines
1import type { EngineInterface } from 'claude-code'
2
3const FLAGS = { plugin: 'switchboard', key: 'flags' } as const
4
5export type Flag = { isOn: boolean; isHidden: boolean }
6
7// Switchboard owns the flags. A mod it has never heard of counts as on, shown.
8export async function flagOf($: EngineInterface, mod: string): Promise<Flag> {
9  const { value } = await $.state.get(FLAGS)
10  const own = value?.[mod]
11
12  return { isOn: own?.isOn ?? true, isHidden: own?.isHidden ?? false }
13}
14
hooks/mask.ts 87 lines
1// Pure text -> text. Everything that looks like private data becomes a gray
2// shade bar. Only what is drawn changes; the transcript the model reads does not.
3
4const BAR = '▒'
5
6const CURRENCY =
7  '(?:RM|MYR|USD|US\\$|SGD|S\\$|CNY|RMB|\\$|¥|¥|€|£)\\s?\\d[\\d,]*(?:\\.\\d+)?(?:\\s?[kKmMbB]\\b)?'
8const CURRENCY_AFTER =
9  '\\d[\\d,]*(?:\\.\\d+)?\\s?(?:元|块钱|块|万元|万|令吉|美金|美元|RM|MYR|USD)'
10const KEYS = [
11  'sk-[A-Za-z0-9_-]{16,}',
12  'AKIA[0-9A-Z]{16}',
13  '(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}',
14  'github_pat_[A-Za-z0-9_]{20,}',
15  'xox[baprs]-[A-Za-z0-9-]{10,}',
16  'AIza[0-9A-Za-z_-]{30,}',
17  'eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]+',
18  'Bearer\\s+[A-Za-z0-9._-]{16,}',
19]
20const COMPANY =
21  "(?:[A-Z][\\w&.'-]*\\s+){0,3}[A-Z][\\w&.'-]*\\s+(?:Sdn\\.?\\s?Bhd\\.?|Bhd\\.?|Inc\\.?|LLC|Ltd\\.?|Pte\\.?\\s?Ltd\\.?|Corp\\.?|GmbH)|[\\u4e00-\\u9fa5]{2,12}(?:有限公司|股份公司|集团|工作室)"
22const HONORIFIC = '[\\u4e00-\\u9fa5]{1,3}(?:先生|小姐|女士|老师|经理|老板)'
23const NAME_LABEL =
24  "(?:客户|顾客|买家|收件人|联系人|姓名|customer|client|buyer|recipient|name)\\s*[::]\\s*([A-Z][\\w'-]+(?:\\s+[A-Z][\\w'-]+){0,2}|[\\u4e00-\\u9fa5]{2,4})"
25
26const FIXED: RegExp[] = [
27  /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g,
28  /\+\d[\d ()-]{7,}\d/g,
29  /(?<![\w.])0\d{1,2}[ -]?\d{3,4}[ -]?\d{4}(?![\w])/g,
30  /(?<![\w.])1[3-9]\d{9}(?![\w])/g,
31  new RegExp(CURRENCY, 'g'),
32  new RegExp(CURRENCY_AFTER, 'g'),
33  ...KEYS.map(k => new RegExp(k, 'g')),
34  /(?:api[_-]?key|token|secret|passw(?:or)?d)["'\s:=]+([A-Za-z0-9._\-/+]{8,})/gi,
35  new RegExp(COMPANY, 'g'),
36  new RegExp(HONORIFIC, 'g'),
37  new RegExp(NAME_LABEL, 'gi'),
38]
39
40const escape = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
41
42export const namesPattern = (names: string): RegExp | undefined => {
43  const list = names
44    .split(/[,,\n]/)
45    .map(one => one.trim())
46    .filter(one => one.length > 0)
47
48  return list.length === 0
49    ? undefined
50    : new RegExp(list.map(escape).join('|'), 'gi')
51}
52
53const bar = (length: number) =>
54  BAR.repeat(Math.min(14, Math.max(4, Math.round(length * 0.8))))
55
56// A capture group, when the pattern has one, is the only part that is hidden
57// (`api_key: <secret>` keeps its label; `name: <Name>` keeps its label).
58const hide = (text: string, pattern: RegExp) =>
59  text.replace(pattern, (...args: unknown[]) => {
60    const whole = String(args[0])
61    const group = typeof args[1] === 'string' ? args[1] : undefined
62
63    return group === undefined
64      ? bar(whole.length)
65      : whole.replace(group, bar(group.length))
66  })
67
68export const maskText = (text: string, names?: RegExp): string => {
69  let out = text
70  for (const pattern of FIXED) out = hide(out, new RegExp(pattern.source, pattern.flags))
71  if (names !== undefined) out = hide(out, new RegExp(names.source, names.flags))
72
73  return out
74}
75
76export const maskDeep = (value: unknown, names?: RegExp): unknown => {
77  if (typeof value === 'string') return maskText(value, names)
78  if (Array.isArray(value)) return value.map(one => maskDeep(one, names))
79  if (value !== null && typeof value === 'object') {
80    return Object.fromEntries(
81      Object.entries(value).map(([key, one]) => [key, maskDeep(one, names)]),
82    )
83  }
84
85  return value
86}
87
types/index.d.ts 11 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'screen-share': { isOn: boolean }
4    // Read-only view of Switchboard's value (it alone writes it).
5    switchboard: {
6      flags: Record<string, { isOn: boolean; isHidden: boolean }>
7      toggles: Record<string, number>
8    }
9  }
10}
11