/grabando prende y apaga el modo grabación: tapa en pantalla mails, teléfonos y montos. Lo que lee Claude no cambia.

Los tres mods de la clase «Mods de Claude Code» de la Academia de Nicolás Dubois. Un mod es código que corre adentro de Claude Code: puede frenar algo antes de que pase, dibujar en pantalla o sumar un comando.
| Mod | Qué hace |
|---|---|
| guardia | No deja leer ni tocar archivos .env (sí el .env.example) ni correr rm -rf. Si la guardia misma falla, frena igual. |
| reloj-cache | Arriba del prompt, cuántos minutos le quedan a la caché y cuántos tokens guarda, con un botón Compactar. Avisa cuando faltan 5 minutos. La duración (60 min) y el aviso (5 min) se cambian en /config. |
| grabando | /grabando prende y apaga el modo grabación: en pantalla tapa mails, teléfonos y montos, en tus mensajes, en las respuestas y en lo que devuelven las herramientas. Se acuerda si estaba prendido. |
En la terminal, adentro de Claude Code (cambiá guardia por reloj-cache o grabando):
/plugin install guardia --marketplace licnicolasdubois/mods-academia
Te pregunta si querés agregar el marketplace (y) y en qué alcance instalarlo: para todos tus proyectos o para uno solo.
Desde la línea de comandos:
claude plugin install guardia --marketplace licnicolasdubois/mods-academia
En la app de escritorio, donde /plugin no anda: el + al lado del prompt → Plugins → Add marketplace con licnicolasdubois/mods-academia → Add plugin.
Cada mod trae sus pruebas:
claude plugin validate guardia
claude plugin test guardia
.catch para que, si falla, frene en vez de dejar pasar.grabando tapa lo que ves en pantalla, no lo que lee Claude: sirve para grabar o compartir pantalla, no es una protección de datos.hooks/register.tsx 74 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4const isOn = atom({ plugin: 'grabando', key: 'isOn' } as const, false)
5
6// Without this note the model reads "recording mode on" and starts withholding the data itself,
7// so nothing gets masked on screen and the answer leaves out what the person asked for.
8const NOTA_AL_MODELO =
9 'Recording mode only masks what the screen shows; you still read the real values. ' +
10 'Answer as usual and write emails, phone numbers and amounts in full: the screen hides them.'
11
12const MAIL = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g
13const MONTO = /(?:US\$|U\$S|USD|UYU|\$U|ARS|EUR|€|\$)\s?\d+(?:[.,]\d+)*|\b\d+(?:[.,]\d+)*\s?(?:USD|dólares|pesos|euros)\b/gi
14// 9 to 13 digits with optional + and separators; a date (2026-10-14, 14/10/2026) is left alone.
15const TELEFONO = /(?<![\w./-])(?!\d{4}-\d{2}-\d{2})(?:\+\d{1,3}[\s-]?)?(?:\d[\s-]?){8,12}\d(?![\w/-])/g
16
17// What the screen shows; the model keeps reading the real text.
18function tapar(text: string): string {
19 return text
20 .replace(MAIL, '[mail oculto]')
21 .replace(MONTO, '[monto oculto]')
22 .replace(TELEFONO, '[teléfono oculto]')
23}
24
25// A tool's output keeps its shape: only its texts change.
26function taparTodo(value: unknown): unknown {
27 if (typeof value === 'string') return tapar(value)
28 if (Array.isArray(value)) return value.map(taparTodo)
29 if (value !== null && typeof value === 'object') {
30 return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, taparTodo(v)]))
31 }
32 return value
33}
34
35export const register: Register = on => {
36 on('session.start', async ($, e, next) => {
37 await $.command.register({
38 name: 'grabando',
39 description: 'Prende o apaga el modo grabación: tapa en pantalla mails, teléfonos y montos',
40 argumentHint: '[on|off]',
41 })
42 // It remembers across sessions: the worst case is starting to record with it off.
43 const stored = (await $.store.get('isOn')) === true
44 await update($, isOn, () => stored)
45 $.ui.status(stored ? 'modo grabación prendido' : undefined)
46 return next(e)
47 })
48
49 on('command.run', { command: 'grabando' }, async ($, e) => {
50 const arg = (e.args ?? '').trim().toLowerCase()
51 const now = arg === 'on' ? true : arg === 'off' ? false : !(await read($, isOn))
52 await update($, isOn, () => now)
53 await $.store.set('isOn', now)
54 $.ui.status(now ? 'modo grabación prendido' : undefined)
55 return now
56 ? { text: 'Modo grabación prendido: en pantalla se tapan mails, teléfonos y montos.', context: [NOTA_AL_MODELO] }
57 : { text: 'Modo grabación apagado.' }
58 })
59
60 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) =>
61 (await read($, isOn)) ? next({ ...e, props: { ...e.props, text: tapar(e.props.text) } }) : next(e),
62 )
63
64 on('ui.render', { component: 'UserMessage' }, async ($, e, next) =>
65 (await read($, isOn)) ? next({ ...e, props: { ...e.props, text: tapar(e.props.text) } }) : next(e),
66 )
67
68 on('ui.render', { component: 'ToolResult' }, async ($, e, next) =>
69 (await read($, isOn)) && !e.props.isErrored
70 ? next({ ...e, props: { ...e.props, output: taparTodo(e.props.output) } })
71 : next(e),
72 )
73}
74types/index.d.ts 8 lines1export type Grabando = { isOn: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'grabando': { isOn: boolean }
6 }
7}
8