A fine turno elenca tutto ciò che è stato scritto fuori dalla repo (Notion, Postpickr, Spreaker, mail, git push) con link e segnalazione degli errori…

Un marketplace di mod per Claude Code, pensato per capire e controllare una sessione: barra della cache e dei consumi, registro di ciò che viene scritto fuori dalla repo, conferma delle proposte, raccolta degli inciampi, modalità rec per le registrazioni e inbox con bozze, etichette e task. Ogni mod è un plugin indipendente, installabile singolarmente con un comando.
Ogni mod si accende e si spegne con un comando /..., che funziona in terminale, nell'app Desktop e da iPad. Lo stato si ricorda tra una sessione e l'altra.
⏳ barra-cache (/barra) — tre righe sopra il prompt. La prima è la memoria della chat: una barra con i token del contesto occupato e quanto è costata la sessione a listino (verde fino al 60%, gialla fino all'80%, rossa oltre, con il suggerimento Nuova chat). La seconda mostra l'*uso delle 5 ore* e l'*uso della settimana* dell'abbonamento, solo dove il motore riporta le cifre. La terza dice da quanto è aperta la sessione e quanto resta di cache calda: la prompt cache dura un'ora dall'ultima richiesta della conversazione principale (le richieste dei subagent non la rinnovano), poi la richiesta successiva riscrive tutto il contesto a prezzo pieno. Con la cache scaduta e almeno 20mila token di contesto compare un avviso rosso. Sotto, una riga con branch, file modificati e commit da pubblicare (letta da git, solo dove $.process esiste: nel cloud non compare) e i bottoni: Commit e push (tasto g), Push (tasto p, compare solo quando ci sono commit da pubblicare e nessuna modifica da committare, senza token) e Handoff (tasto n), che fa scrivere al modello un riassunto di ripartenza in ~/.claude/handoffs/, svuota la chat con /clear e riparte da quel file. Dove la banda non viene disegnata (ad esempio l'app su iPad) restano /cache, che mostra minuti rimasti, contesto, costo dell'ultimo turno e quota di cache letta, /push e /handoff (alias /nuova). I comandi sono anche file statici in barra-cache/commands/, perché l'elenco dello slash dell'app non mostra quelli registrati a runtime; l'hook risponde prima del modello, quindi non costano token.
📒 scritture-esterne (/scritture) — a fine turno elenca tutto ciò che il turno ha scritto fuori dalla repo: Notion, Postpickr, Spreaker, Gmail, Calendar, Drive, git push, gh, curl -X POST e gli script con --applica o --elimina. Ogni riga ha servizio, azione, bersaglio, un link apri se la risposta ne contiene uno, e una croce rossa se la chiamata è fallita o negata. Le letture non compaiono. Il registro sparisce all'inizio del turno successivo. Riconosce dal nome i connettori più comuni; per quelli con id opaco serve l'opzione servizi del plugin.
✅ conferma-proposte (/conferma) — quando l'ultima risposta di Claude chiude con una proposta o una domanda di conferma (una domanda, oppure formule come "procedo", "vuoi che", "confermi"), mostra sopra il prompt il passaggio che propone e due bottoni: Sì, procedi (tasto s) e No, fermati (tasto x). Il bottone invia la risposta come se l'avessi scritta tu. Non usa il modello, quindi non consuma token. Funziona anche nel cloud e da iPad.
🛠 correggi (/correggi) — durante la sessione annota gli inciampi: tool che falliscono, azioni negate dal sistema di permessi e le tue correzioni ("non vedo", "hai sbagliato", "riprova"). Con almeno due inciampi compare una riga con il conteggio, le skill usate e il bottone Proponi correzione (tasto l), che chiede a Claude di individuare la causa e proporre la modifica esatta alla skill o al file di istruzioni, senza applicarla. /correggi elenco mostra gli inciampi raccolti. Consuma token solo quando premi il bottone.
🔴 rec (/rec) — la modalità per registrare un video o lavorare in una sessione live con ospiti. Maschera a schermo chiavi e valori dei .env, email, nomi, telefoni, indirizzi, codice fiscale, partita IVA, IBAN, importi in euro e cifre vicino a parole come fatturato, margine, compenso, preventivo. I risultati di posta, chat, task, file, calendario, Notion e strumenti di pagamento si disegnano nascosti. Claude continua a lavorare sui dati reali: cambia solo ciò che si vede (se modifica un file da 29 a 39 euro, il file cambia davvero, lo schermo no). Tiene chiusi i file privati (.env, credenziali, fatture, contratti, preventivi, buste paga) e gli strumenti di pagamento, e ogni prompt porta una nota nascosta che chiede a Claude di usare segnaposto al posto di nomi e cifre. Un ● REC rosso sopra il prompt e nel piè di pagina ricorda che è acceso. /rec alterna, /rec rigoroso maschera anche ogni cifra grande, /rec off spegne, /rec config crea ~/.claude/mods-data/rec/config.json per il tuo nome (nomiVisibili), le persone da nascondere (nomiNascosti), le cartelle private (percorsiPrivati) e gli strumenti extra (strumentiAffari, strumentiChiusi). Limiti: cambia ciò che è disegnato, non ciò che è memorizzato; il riconoscimento per pattern non prende tutto quello che è scritto a parole, quindi riguarda il girato prima di pubblicarlo; i titoli delle chat nella barra laterale dell'app Desktop non si possono mascherare. Adattata da recording-mode di Nate Herk (MIT, vedi rec/NOTICE.md).
📬 posta (posta o /posta) — apre un pannello con la inbox Gmail: l'elenco delle ultime 12 mail per account (una finestra di 5 righe che scorre con j e k), i bottoni e, sotto, il testo intero della mail selezionata (tutti i messaggi del thread, dal più recente, con allegati), letto da Gmail senza modello. I bottoni sono Bozza (b), Label (l), Bozza+Label (m), Task (t), più Aggiorna (r) e Chiudi (x). Le quattro azioni girano dentro la mod, senza subagent: il modello riceve il thread senza alcun tool e sceglie solo i contenuti, mentre le scritture su Gmail e Notion le fa il codice dopo averle validate (la chat resta pulita: l'esito compare nel pannello e in un avviso). Bozza scrive con le regole di ~/.claude/mods-data/posta/sistematore.md e crea una bozza di risposta (la mail non viene mai inviata). Label fa scegliere al modello economico un'etichetta tra quelle esistenti e la applica solo se il nome coincide (non ne crea). Task fa proporre al modello economico titolo, scadenza, urgenza, importanza, impegno e contesto, li riporta ai valori ammessi (scadenza non valida o passata: domani alle 09:00) e crea la pagina nel database Tasks di Notion con il link alla mail. Solo Label e Bozza+Label tolgono la mail dall'inbox (finisce nella cartella dell'etichetta); Bozza e Task la lasciano in inbox. I lavori in corso mostrano uno spinner, quelli riusciti spariscono dopo 8 secondi e quelli falliti si rilanciano con Riprova (y). Nel dettaglio compare l'ultimo messaggio del thread; i precedenti sono righe ripiegate e si aprono con Mostra precedenti (p). Scrivendo posta senza barra il pannello si apre senza avviso dell'app e senza token; posta off lo chiude. Gli account sono in DEFAULT_ACCOUNTS e si sostituiscono con la chiave accounts dello store. In modalità Auto anche le chiamate della mod passano dal classificatore, che sulle scritture non dà verdetto: vanno autorizzate in permissions.allow le letture Gmail (search_threads, get_thread, list_labels) e le quattro scritture (create_draft, label_thread, unlabel_thread di Gmail, notion-create-pages di Notion), con il prefisso mcp__<id connettore>__. Un errore resta visibile solo sulla mail a cui appartiene e sparisce quando la stessa azione riesce.
claude --version.Una mod è codice eseguito con i tuoi permessi: può leggere e scrivere file, avviare processi e vedere prompt e tool call della sessione. Il sorgente di ogni mod è in hooks/: leggilo prima dell'installazione.
claude plugin marketplace add andreabrugnoli/mods
claude plugin install barra-cache@andrea-mods
claude plugin install scritture-esterne@andrea-mods
claude plugin install conferma-proposte@andrea-mods
claude plugin install correggi@andrea-mods
claude plugin install rec@andrea-mods
claude plugin install posta@andrea-mods
claude.Verifica: apri /plugin e controlla che le mod risultino attive. Per provare una mod in una sola sessione senza installarla, clona la repo e avvia claude --plugin-dir ./mods/barra-cache. Per disattivarla, usa il tab Installed di /plugin.
Le sessioni cloud (claude.ai/code, app per iPad) non leggono le tue impostazioni locali: partono da un contenitore pulito e caricano i plugin dichiarati nella repo su cui lavori. Per attivare una mod in una repo, aggiungi a .claude/settings.json di quella repo:
{
"extraKnownMarketplaces": {
"andrea-mods": { "source": { "source": "github", "repo": "andreabrugnoli/mods" } }
},
"enabledPlugins": { "barra-cache@andrea-mods": true, "rec@andrea-mods": true }
}
Lo fa per te scripts/abilita-cloud.sh <percorso-repo> [mod ...], che unisce la voce alle impostazioni esistenti. Se il tuo gitignore (anche globale) esclude .claude/settings.json, aggiungilo con git add -f .claude/settings.json, poi commit e push. La sessione cloud legge il file dal branch.
Su iPad la banda sopra il prompt può non essere disegnata: restano /cache, /push e /nuova, e tutti i comandi on/off (/barra, /scritture, /conferma, /correggi, /rec) funzionano uguale. Lo stato ($.store) e i riassunti in ~/.claude/handoffs/ vivono nel contenitore della sessione e si perdono alla sua chiusura.
Accendere e spegnere (senza argomento alternano; accettano anche on e off):
/barra: la barra sopra il prompt./scritture: il registro delle scritture esterne./conferma: i bottoni Sì e No sulle proposte./correggi: la raccolta degli inciampi (/correggi elenco li mostra)./rec: la modalità registrazione (/rec rigoroso, /rec off, /rec config).posta o /posta: la inbox con i bottoni Bozza, Label e Task (posta aggiorna, posta off).Altri comandi di barra-cache:
/cache: minuti di cache rimasti, contesto occupato, costo dell'ultimo turno e quota di cache letta./push: pubblica i commit del branch corrente./nuova: riassume la sessione in un file e riparte da una chat pulita.Durata della cache in barra-cache: parte da un'ora (TTL_MS) e si corregge da sola. Dopo una pausa di almeno 6 minuti guarda la quota di cache letta: se la cache è stata riscritta, passa a 5 minuti e lo ricorda tra le sessioni; se è stata letta, conferma l'ora.
Soglie dei colori in barra-cache: CONTEXT_WARN, CONTEXT_BAD, COST_WARN, CACHE_OK e CACHE_BAD, in cima allo stesso file.
Nomi dei connettori in scritture-esterne: opzione servizi del plugin.
Per modificare una mod, clona la repo e caricala con claude --plugin-dir: ogni salvataggio ricarica il modulo nella sessione aperta.
mods/
├── .claude-plugin/marketplace.json # elenco delle mod installabili
├── barra-cache/
├── scritture-esterne/
├── conferma-proposte/
├── correggi/
├── rec/
├── posta/
└── scripts/abilita-cloud.sh
Ogni cartella di mod è un plugin completo: .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.js e tests/ (eseguibili con claude plugin test ./<mod>). claude plugin validate ./<mod> elenca gli eventi intercettati e le chiamate al motore.
Licenza: MIT.
hooks/register.js 191 lines1// Quante righe mostrare al massimo nella banda
2const MAX_ROWS = 6
3
4// Nomi leggibili dei server MCP riconosciuti dal nome; i connettori con id opaco si nominano nelle opzioni
5const KNOWN = [
6 [/notion/i, 'Notion'],
7 [/spreaker/i, 'Spreaker'],
8 [/postpickr/i, 'Postpickr'],
9 [/gmail/i, 'Gmail'],
10 [/calendar/i, 'Calendar'],
11 [/drive/i, 'Drive'],
12 [/vercel/i, 'Vercel'],
13 [/stripe/i, 'Stripe'],
14 [/github/i, 'GitHub'],
15 [/slack/i, 'Slack'],
16 [/linear/i, 'Linear'],
17]
18const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-/i
19
20// Dall'opzione "servizi" ("id=Nome, id=Nome") alla mappa id -> nome
21export function parseServices(text) {
22 const map = {}
23 for (const part of String(text ?? '').split(',')) {
24 const [id, ...name] = part.split('=')
25 if (id && name.length) map[id.trim()] = name.join('=').trim()
26 }
27 return map
28}
29
30// Il nome da mostrare per un server: prima le opzioni, poi il nome noto, poi l'id accorciato
31export function serviceLabel(server, custom = {}) {
32 if (custom[server]) return custom[server]
33 const known = KNOWN.find(([re]) => re.test(server))
34 if (known) return known[1]
35 return UUID.test(server) ? server.slice(0, 8) : server
36}
37
38// Un tool MCP scrive se il nome contiene un verbo di scrittura e non uno di sola lettura
39const READ_VERB = /^(get|list|search|fetch|query|read|find|check|validate|resolve|download)/
40const WRITE_VERB = /(create|update|delete|trash|send|schedule|reschedule|publish|post|upload|move|duplicate|reply|forward|label|write|add|remove|set|save|stop|spawn)/
41
42// Le chiavi dell'input che meglio dicono "su cosa" è stata fatta la scrittura
43const TARGET_KEYS = ['title', 'name', 'subject', 'messaggio', 'page_id', 'id', 'project_id', 'url']
44
45// Le scritture del turno in corso, e quelle del turno concluso da mostrare
46let pending = []
47let shown = []
48
49// Accorcia un testo su una riga
50function clip(text, n) {
51 const one = String(text).replace(/\s+/g, ' ').trim()
52 return one.length > n ? one.slice(0, n - 1) + '…' : one
53}
54
55// Dice se una chiamata è una scrittura esterna e la descrive, altrimenti null
56export function describe(e, custom = {}) {
57 if (e.tool === 'Bash') {
58 const cmd = e.command ?? ''
59 if (/\bgit\s+push\b/.test(cmd)) return { service: 'git', action: 'push', target: clip(cmd, 60) }
60 if (/\bgh\s+(pr|issue|release|repo)\s+(create|merge|close|comment|edit|delete)/.test(cmd)) return { service: 'GitHub', action: 'modifica', target: clip(cmd, 60) }
61 if (/\bcurl\b.*-X\s*(POST|PUT|PATCH|DELETE)/.test(cmd)) return { service: 'HTTP', action: 'scrittura', target: clip(cmd, 60) }
62 if (/--(applica|elimina|apply|delete)\b/.test(cmd)) return { service: 'script', action: 'applica', target: clip(cmd, 60) }
63 return null
64 }
65 const m = /^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/.exec(e.tool)
66 if (!m) return null
67 const [, server, tool] = m
68 if (READ_VERB.test(tool) || !WRITE_VERB.test(tool)) return null
69 const key = TARGET_KEYS.find((k) => e[k] !== undefined && typeof e[k] !== 'object')
70 return {
71 service: serviceLabel(server, custom),
72 action: tool.replace(/^notion-/, '').replace(/_/g, ' '),
73 target: key ? clip(e[key], 50) : '',
74 }
75}
76
77// Il primo link https nel risultato del tool, se c'è
78export function findLink(text) {
79 const m = /https:\/\/[^\s"'<>)\]]+/.exec(text ?? '')
80 return m ? m[0].replace(/[.,;]+$/, '') : null
81}
82
83// Un esito è dubbio se il tool ha segnalato errore o il testo dice che è fallito
84export function isBad(result) {
85 if (result.isError === true) return true
86 return /"?(error|errore|failed|unauthorized|forbidden)"?\s*[:=]/i.test(result.text ?? '')
87}
88
89// Il nuovo stato dopo un comando: "on" accende, "off" spegne, senza argomento alterna
90export function parseToggle(args, current) {
91 const arg = String(args ?? '').trim().toLowerCase()
92 if (/^(on|acceso|attiva|si|sì)$/.test(arg)) return true
93 if (/^(off|spento|disattiva|no)$/.test(arg)) return false
94 return !current
95}
96
97// Acceso o spento: lo decide /scritture e resta nello store tra una sessione e l'altra
98let enabled = true
99
100async function runScritture($, e) {
101 enabled = parseToggle(e.args, enabled)
102 pending = []
103 shown = []
104 try {
105 await $.store.set('attiva', enabled)
106 } catch {}
107 $.ui.invalidate('ui.render')
108 return { text: 'scritture-esterne · ' + (enabled ? 'acceso' : 'spento') }
109}
110
111export function register(on, options) {
112 // I nomi dei connettori con id opaco, scritti dall'utente nelle opzioni
113 const custom = parseServices(options?.servizi)
114
115 on('session.start', async ($, e, next) => {
116 try {
117 if ((await $.store.get('attiva')) === false) enabled = false
118 } catch {}
119 await $.command.register({ name: 'scritture', description: 'Accende o spegne il registro delle scritture esterne', argumentHint: '[on|off]', immediate: true })
120 return next(e)
121 })
122
123 on('command.run', { command: 'scritture' }, runScritture)
124 // Il comando statico del plugin si chiama anche scritture-esterne:scritture
125 on('command.run', { command: 'scritture-esterne:scritture' }, runScritture)
126
127 // Ogni chiamata: la lasciamo passare, poi annotiamo che cosa è successo
128 on('tool.call', async ($, e, next) => {
129 if (!enabled) return next(e)
130 const what = describe(e, custom)
131 const ran = await next(e)
132 if (what && !e.agentId) {
133 const denied = ran.deny !== undefined
134 pending.push({
135 ...what,
136 state: denied ? 'negata' : isBad(ran) ? 'errore' : 'ok',
137 link: denied ? null : findLink(ran.text),
138 })
139 }
140 return ran
141 })
142
143 // Un nuovo turno cancella il registro del precedente
144 on('turn.start', ($, e, next) => {
145 if (!e.agentId) {
146 pending = []
147 if (shown.length) {
148 shown = []
149 $.ui.invalidate('ui.render')
150 }
151 }
152 return next(e)
153 })
154
155 // A fine turno il registro diventa visibile
156 on('turn.complete', ($, e, next) => {
157 if (enabled && !e.agentId) {
158 shown = pending
159 pending = []
160 $.ui.invalidate('ui.render')
161 }
162 return next(e)
163 })
164
165 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
166 const theirs = await next(e)
167 if (!enabled || !shown.length || e.props.isWorking) return theirs
168 const { Box, Text, Link } = $.ui.resolve(e)
169 const bad = shown.filter((r) => r.state !== 'ok').length
170 const head = Text({
171 color: bad ? 'red' : undefined,
172 dimColor: !bad,
173 children: ['scritto fuori dalla repo · ' + shown.length + (shown.length === 1 ? ' azione' : ' azioni') + (bad ? ' · ' + bad + ' da controllare' : '')],
174 })
175 const rows = shown.slice(0, MAX_ROWS).map((r) =>
176 Box({
177 flexDirection: 'row',
178 children: [
179 Text({ color: r.state === 'ok' ? 'green' : 'red', children: [r.state === 'ok' ? '✓ ' : '✗ '] }),
180 Text({ children: [r.service + ' · ' + r.action + (r.target ? ' · ' + r.target : '')] }),
181 r.state !== 'ok' && Text({ color: 'red', children: [' (' + r.state + ')'] }),
182 r.link && Text({ children: [' '] }),
183 r.link && Link({ href: r.link, label: 'apri' }),
184 ].filter(Boolean),
185 }),
186 )
187 const more = shown.length > MAX_ROWS ? [Text({ dimColor: true, children: ['… e altre ' + (shown.length - MAX_ROWS)] })] : []
188 return Box({ flexDirection: 'column', children: [head, ...rows, ...more, theirs].filter(Boolean) })
189 })
190}
191