Salvataggi automatici del progetto prima che Claude modifichi i file: /salvataggi per tornare a quando funzionava, /salva per un salvataggio tuo.

Cinque mod gratuite per chi usa Claude Code per costruire i propri progetti (un sito, un'app, un'automazione) anche senza saper programmare.
| Mod | In una riga | |
|---|---|---|
| 💾 | save-point | Claude ha rotto qualcosa? Torni a quando funzionava con un clic. |
| 💡 | cosa-hai-fatto | Dopo ogni modifica ti spiega in parole semplici cosa ha fatto Claude e come provarlo. |
| ⛽ | benzina | Vedi quanto ti resta del piano Claude prima che si fermi, e quando si ricarica. |
| ⚡ | quick-replies | Pulsanti per le risposte che scrivi sempre: "Continua", "Procedi", "Non funziona, controlla tu". |
| 🌐 | localhost | Vedi le app che hai acceso sul computer, aprile nel browser o spegnile. |
Puoi installarne una sola, alcune o tutte e cinque: sono indipendenti.
Una mod è un piccolo plugin che cambia come Claude Code si comporta e cosa ti mostra: una riga sopra il punto dove scrivi, un pannello laterale, un pulsante, un avviso. Non cambia il modello e non legge niente fuori dal tuo computer: gira dentro Claude Code, sul tuo Mac o PC.
Dove le vedi:
/salvataggi (save-point) e /localhost. claude --version ` Se è più vecchio, aggiornalo con: ` claude update ``xcode-select --install. Su Linux: installa il pacchetto git. cd percorso/del/tuo/progetto claude `` /plugin install save-point --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install cosa-hai-fatto --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install benzina --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install quick-replies --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install localhost --marketplace cavallinilorenzo/claude-code-mods ``Add marketplace?: rispondi y.Installed <nome>. Plugin is now active. La mod è già attiva, non serve riavviare.In alternativa, dal terminale normale (fuori da Claude Code):
claude plugin marketplace add cavallinilorenzo/claude-code-mods
claude plugin install save-point@lorenzo-mods
claude plugin install cosa-hai-fatto@lorenzo-mods
claude plugin install benzina@lorenzo-mods
claude plugin install quick-replies@lorenzo-mods
claude plugin install localhost@lorenzo-mods
Il problema: chiedi una modifica, Claude cambia dieci file e l'app non parte più. Non sai cosa ha toccato e non sai come tornare indietro.
Cosa fa: ogni volta che Claude sta per modificare i file, save-point salva com'era il progetto un attimo prima. Lo fa da solo, una volta per ogni tua richiesta, e il salvataggio prende il nome della richiesta.
Come si usa:
| Comando | Cosa succede |
|---|---|
/salvataggi | Apre l'elenco dei salvataggi. Accanto a ognuno c'è Torna qui: lo premi, confermi con Sì, torna qui e tutti i file tornano com'erano in quel momento. |
/salva | Salva adesso, a mano. |
/salva funziona il login | Salva adesso con un nome tuo: comparirà come ⭐ funziona il login. |
Cosa vedi in /salvataggi:
oggi 15:42 Prima di: «aggiungi il carrello» [ Torna qui ]
oggi 15:10 ⭐ funziona il login [ Torna qui ]
oggi 14:31 Prima di: «cambia i colori del sito» [ Torna qui ]
Da sapere:
~/.claude/save-points/.node_modules, .venv, venv, __pycache__, .next, dist, build, target, .cache), i file .log e i file che il tuo progetto ignora già con .gitignore.~/.claude/save-points/.Il problema: Claude risponde con nomi di file, funzioni e comandi. Tu vuoi solo sapere cosa è cambiato e se funziona.
Cosa fa: quando una risposta di Claude ha modificato dei file, sopra il prompt compare un riassunto in parole semplici, senza gergo tecnico, con un passo concreto per provarlo.
💡 In breve: Ora i clienti possono pagare con la carta e il carrello mostra le spese di spedizione.
👉 Prova: aggiungi un prodotto al carrello e premi «Paga».
[ Spiegamelo meglio ] [ Nascondi ]
Da sapere:
english, español).Il problema: stai lavorando e all'improvviso Claude si ferma perché hai finito il limite del tuo piano. Oppure la chat è così lunga che Claude inizia a dimenticare le cose.
Cosa fa: mostra sopra il prompt quanto ti resta, come l'indicatore della benzina:
⛽ 5 ore ▰▰▰▱▱▱▱▱ 38% · ricarica alle 16:40 Settimana ▰▰▰▰▰▰▱▱ 80% Chat ▰▰▰▰▰▱▱▱ 64%
Quando serve ti dà un consiglio in chiaro:
Ti avvisa anche con una notifica quando superi l'80% e il 95% di un limite.
| Comando | Cosa succede |
|---|---|
/benzina | Nasconde l'indicatore, o lo fa ricomparire. La scelta resta salvata. |
Da sapere: i limiti delle 5 ore e della settimana compaiono solo con un abbonamento Claude (Pro o Max) e dopo la prima risposta di Claude. Se usi una chiave API vedi solo la Chat.
Il problema: scrivi cento volte al giorno "continua", "procedi", "ok va bene".
Cosa fa: dopo ogni risposta di Claude, sopra il prompt compaiono dei pulsanti con le risposte che usi sempre. Un clic e la risposta parte.
[ Continua ] [ Procedi ] [ Ok a tutte le tue raccomandazioni ] [ Non funziona, controlla tu e sistemalo ]
Le risposte si cambiano dalle impostazioni: scrivile separate da |, fino a 9. Ad esempio:
Continua|Procedi|Spiegamelo in parole semplici|Avviamelo così lo provo
Il problema: Claude ti dice "l'app è su localhost:5173". Ma quale? Quante ne hai accese? Come le spegni?
Cosa fa: /localhost apre un pannello con tutte le app che hai acceso sul computer (vite, next, django, flask, uvicorn…), la porta, la cartella da cui sono partite e due pulsanti:
:5173 node ~/progetti/negozio apri [ ferma ]
:8000 Python ~/progetti/negozio/backend apri [ ferma ]
:3000 node ~/progetti/vecchio-test apri [ ferma ]
[ Ferma quelli di questo progetto (2) ] [ Ferma tutti ] [ Aggiorna ]
Da sapere: funziona su Mac e Linux.
Due mod hanno impostazioni:
| Mod | Impostazione | Default | ||||
|---|---|---|---|---|---|---|
| quick-replies | Risposte rapide (separate da `\ | `) | `Continua\ | Procedi\ | Ok a tutte le tue raccomandazioni\ | Non funziona, controlla tu e sistemalo` |
| cosa-hai-fatto | Lingua delle spiegazioni | italiano |
Le scegli la prima volta, nella schermata che compare durante l'installazione. Per cambiarle dopo, dal terminale:
echo '{"replies": "Continua|Procedi|Spiegamelo in parole semplici"}' | claude plugin configure quick-replies@lorenzo-mods --values-stdin
echo '{"language": "english"}' | claude plugin configure cosa-hai-fatto@lorenzo-mods --values-stdin
Per vedere i valori attuali: claude plugin configure quick-replies@lorenzo-mods.
Dal terminale:
claude plugin update save-point@lorenzo-mods # prende l'ultima versione
claude plugin disable benzina@lorenzo-mods # la spegne senza toglierla
claude plugin enable benzina@lorenzo-mods # la riaccende
claude plugin uninstall localhost@lorenzo-mods # la toglie
Dentro Claude Code, /plugin apre lo stesso menu con i pulsanti.
Devo saper programmare? No. Si installano incollando una riga e si usano con pulsanti e comandi in italiano.
Costano qualcosa? Le mod sono gratis. Solo cosa-hai-fatto fa una piccola chiamata a Claude Haiku dopo le risposte che modificano file, che conta sul tuo piano Claude come qualsiasi altro messaggio.
Mandano i miei dati da qualche parte? No. Lavorano sul tuo computer. save-point salva i file in ~/.claude/save-points/. cosa-hai-fatto manda a Claude (come fa già Claude Code) la tua richiesta, i nomi dei file modificati e la risposta di Claude, per scrivere il riassunto. Il codice è tutto in questa pagina: puoi leggerlo.
save-point sostituisce git o GitHub? No. È una rete di sicurezza per tornare indietro mentre lavori con Claude. Se vuoi pubblicare il progetto o lavorarci con altre persone, git e GitHub restano lo strumento giusto.
E il tasto Esc due volte di Claude Code ("rewind")? Il rewind di Claude Code annulla le modifiche fatte con i suoi strumenti di modifica dei file. save-point salva tutta la cartella, quindi copre anche quello che cambia per altre vie (comandi, installazioni, script), e i salvataggi non dipendono dalla conversazione: li ritrovi anche in una chat nuova.
Funzionano nell'app desktop? Sì, nella scheda Code. Si installano dal terminale una volta sola e poi le trovi anche lì.
Funzionano su Windows? benzina, cosa-hai-fatto e quick-replies sì. save-point e localhost sono pensate per Mac e Linux: su Windows non le ho provate.
| Cosa vedi | Cosa fare |
|---|---|
Marketplace file not found | Controlla di aver scritto bene cavallinilorenzo/claude-code-mods. |
Plugin "…" not found in marketplace | Controlla il nome della mod: save-point, cosa-hai-fatto, benzina, quick-replies, localhost. |
/plugin install non esiste nell'app desktop | È normale: installale dal terminale, poi le trovi anche nell'app. |
| save-point dice che non salva la home | Hai avviato Claude nella tua home. Chiudi, entra nella cartella del progetto (cd …) e riavvia claude. |
| save-point dice che serve git | Mac: xcode-select --install. Linux: installa il pacchetto git. |
| benzina mostra solo la Chat | Usi una chiave API invece di un abbonamento, oppure Claude non ha ancora risposto in questa sessione. |
| La banda sopra il prompt non si vede | Si può ridurre con [-] o ctrl+x ctrl+a: premi di nuovo per riaprirla. |
| Una mod non fa niente | Aggiorna Claude Code (claude update) e controlla con claude plugin list che sia attiva. |
Ogni mod è una cartella in mods/ con:
.claude-plugin/plugin.json, il nome, la descrizione e le impostazioni;hooks/register.tsx, il codice della mod;hooks/*.test.ts(x), i test;types/index.d.ts, i valori che la mod tiene in memoria.Per provarne una modificata senza installarla:
claude --plugin-dir ./mods/save-point
Per controllarla e lanciare i test:
claude plugin validate ./mods/save-point
claude plugin test ./mods/save-point
Idee e segnalazioni: apri una issue.
MIT: puoi usarle, modificarle e ricondividerle, mantenendo la nota di copyright.
Fatto da Lorenzo Cavallini.
hooks/register.tsx 252 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SavePoint } from '../types'
5import {
6 EXCLUDES,
7 LOG_FORMAT,
8 commitMessage,
9 formatWhen,
10 gitArgv,
11 isUnsafeFolder,
12 parseLog,
13 promptLabel,
14 storeDir,
15} from './savepoints'
16
17const PANE = 'salvataggi'
18const SHOWN = 30
19// The tools that change files: the first of them in a turn takes a save point first.
20const CHANGING_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'Bash', 'PowerShell'])
21
22const points = atom({ plugin: 'save-point', key: 'points' } as const, [])
23const confirming = atom({ plugin: 'save-point', key: 'confirming' } as const, null)
24const isBusy = atom({ plugin: 'save-point', key: 'isBusy' } as const, false)
25
26type Place = { cwd: string; store: string }
27
28async function place($: EngineInterface): Promise<Place | null> {
29 const cwd = await $.session.cwd()
30 const home = await $.env.get('HOME')
31 if (home === undefined || isUnsafeFolder(cwd, home)) {
32 return null
33 }
34 return { cwd, store: storeDir(home, cwd) }
35}
36
37async function git($: EngineInterface, where: Place, args: string[], timeoutMs = 120_000) {
38 return $.process.run(gitArgv(where.store, where.cwd, args), { cwd: where.cwd, timeoutMs })
39}
40
41async function ensureStore($: EngineInterface, where: Place): Promise<void> {
42 if (await $.fs.exists(`${where.store}/HEAD`)) {
43 return
44 }
45 await $.process.run(['git', 'init', '--bare', '--quiet', where.store])
46 await $.process.run(['git', `--git-dir=${where.store}`, 'config', 'core.bare', 'false'])
47 await $.fs.write(`${where.store}/info/exclude`, `${EXCLUDES.join('\n')}\n`)
48}
49
50async function refresh($: EngineInterface): Promise<SavePoint[]> {
51 const where = await place($)
52 if (where === null || !(await $.fs.exists(`${where.store}/HEAD`))) {
53 await update($, points, () => [])
54 return []
55 }
56 const log = await git($, where, ['log', `-n${SHOWN}`, `--format=${LOG_FORMAT}`]).catch(() => null)
57 const found = log !== null && log.exitCode === 0 ? parseLog(log.stdout) : []
58 await update($, points, () => found)
59 return found
60}
61
62type Saved = 'saved' | 'unchanged' | 'unsafe' | 'failed'
63
64async function save($: EngineInterface, kind: SavePoint['kind'], label: string): Promise<Saved> {
65 const where = await place($)
66 if (where === null) {
67 return 'unsafe'
68 }
69 try {
70 await ensureStore($, where)
71 const added = await git($, where, ['add', '--all', '--', ':/'])
72 if (added.exitCode !== 0) {
73 return 'failed'
74 }
75 const hasHead = (await git($, where, ['rev-parse', '--verify', '--quiet', 'HEAD'])).exitCode === 0
76 const isSame = hasHead && (await git($, where, ['diff', '--cached', '--quiet'])).exitCode === 0
77 // An automatic save point of nothing new would only crowd the list; one the person asked for is kept.
78 if (isSame && kind !== 'manual') {
79 return 'unchanged'
80 }
81 const committed = await git($, where, ['commit', '--quiet', '--no-verify', '--allow-empty', '-m', commitMessage(kind, label)])
82 if (committed.exitCode !== 0) {
83 return 'failed'
84 }
85 await refresh($)
86 const now = await $.clock.now()
87 $.ui.status(`💾 salvato ${formatWhen(now, now)}`)
88 return isSame ? 'unchanged' : 'saved'
89 } catch {
90 return 'failed'
91 }
92}
93
94async function restore($: EngineInterface, point: SavePoint): Promise<void> {
95 const where = await place($)
96 if (where === null) {
97 return
98 }
99 await update($, isBusy, () => true)
100 try {
101 const now = await $.clock.now()
102 const when = formatWhen(point.savedAt, now)
103 // What is there now is saved first, so going back can itself be undone.
104 await save($, 'restore', `Com'era prima di tornare a ${when}`)
105 const restored = await git($, where, ['restore', `--source=${point.sha}`, '--staged', '--worktree', '--', ':/'])
106 if (restored.exitCode !== 0) {
107 $.ui.toast(`Non sono riuscito a tornare al salvataggio: ${restored.stderr.trim().slice(0, 200)}`)
108 return
109 }
110 await save($, 'restore', `Tornato a ${when}: ${point.label}`)
111 await $.session
112 .append({
113 message: {
114 type: 'user',
115 content: [
116 {
117 type: 'text',
118 text:
119 `[save-point] L'utente ha riportato i file del progetto a com'erano ${when} (${point.label}). ` +
120 'Le modifiche fatte dopo non ci sono più: rileggi i file prima di modificarli.',
121 },
122 ],
123 },
124 })
125 .catch(() => undefined)
126 $.ui.toast(`Fatto: i file sono tornati a ${when}. Se cambi idea, il primo salvataggio della lista è com'erano prima.`)
127 } finally {
128 await update($, confirming, () => null)
129 await update($, isBusy, () => false)
130 }
131}
132
133function describe(result: Saved): string {
134 switch (result) {
135 case 'saved':
136 return 'Salvato. Lo trovi in /salvataggi.'
137 case 'unchanged':
138 return 'Salvato (nessun file cambiato dall\'ultimo salvataggio).'
139 case 'unsafe':
140 return 'Non salvo questa cartella: è la tua home o la radice del disco. Apri Claude dentro la cartella del progetto.'
141 case 'failed':
142 return 'Non sono riuscito a salvare: serve git installato (su Mac: xcode-select --install).'
143 }
144}
145
146export const register: Register = on => {
147 let prompt = ''
148 let isSavedThisTurn = false
149
150 on('session.start', async ($, e, next) => {
151 await $.command.register({
152 name: 'salva',
153 description: 'Salva adesso com\'è il progetto (es. /salva funziona il login)',
154 argumentHint: '[nome]',
155 })
156 await $.command.register({
157 name: 'salvataggi',
158 description: 'I salvataggi del progetto: torna a com\'era prima di una richiesta',
159 })
160 void refresh($)
161
162 return next(e)
163 })
164
165 on('turn.start', ($, e, next) => {
166 prompt = e.text
167 isSavedThisTurn = false
168
169 return next(e)
170 })
171
172 on('tool.call', async ($, e, next) => {
173 if (!isSavedThisTurn && CHANGING_TOOLS.has(String(e.tool))) {
174 isSavedThisTurn = true
175 await save($, 'auto', promptLabel(prompt))
176 }
177
178 return next(e)
179 })
180
181 on('command.run', { command: 'salva' }, async ($, e) => {
182 const name = e.args.trim()
183 const result = await save($, 'manual', name.length > 0 ? `⭐ ${name}` : '⭐ Salvato a mano')
184
185 return { text: describe(result) }
186 })
187
188 on('command.run', { command: 'salvataggi' }, async $ => {
189 const found = await refresh($)
190 await update($, confirming, () => null)
191 await $.ui.open({ id: PANE, title: 'salvataggi', focus: true, closeOnEscape: true })
192
193 return { text: `${found.length} salvataggi per questo progetto.` }
194 })
195
196 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
197 const { Box, Button, Text } = $.ui.resolve(e)
198 const list = await read($, points)
199 const pending = await read($, confirming)
200 const busy = await read($, isBusy)
201 const now = await $.clock.now()
202
203 return (
204 <Box flexDirection="column">
205 <Text dimColor>
206 Prima che Claude modifichi i file salvo com'era il progetto. Scegli un punto per tornarci.
207 </Text>
208 <Box flexDirection="row" columnGap={1} marginY={1}>
209 <Button
210 key="save-now"
211 label="⭐ Salva adesso"
212 variant="primary"
213 onPress={async () => {
214 $.ui.toast(describe(await save($, 'manual', '⭐ Salvato a mano')))
215 }}
216 />
217 </Box>
218 {busy && <Text color="warning">Sto riportando i file indietro…</Text>}
219 {list.length === 0 && (
220 <Text dimColor>Ancora nessun salvataggio: il primo arriva quando Claude inizia a modificare i file.</Text>
221 )}
222 {list.map((point, index) => (
223 <Box key={`row-${point.sha}`} flexDirection="column">
224 <Box flexDirection="row" columnGap={1}>
225 <Text dimColor>{formatWhen(point.savedAt, now).padEnd(11)}</Text>
226 <Box flexGrow={1}>
227 <Text wrap="truncate-end" bold={point.kind === 'manual'} dimColor={point.kind === 'restore'}>
228 {point.label}
229 </Text>
230 </Box>
231 {pending !== point.sha && !busy && (
232 <Button
233 key={`back-${index}`}
234 label="Torna qui"
235 onPress={() => update($, confirming, () => point.sha)}
236 />
237 )}
238 </Box>
239 {pending === point.sha && !busy && (
240 <Box flexDirection="row" columnGap={1} marginLeft={2}>
241 <Text color="warning">Riporto tutti i file a {formatWhen(point.savedAt, now)}?</Text>
242 <Button key="confirm" label="Sì, torna qui" variant="primary" onPress={() => restore($, point)} />
243 <Button key="cancel" label="Annulla" onPress={() => update($, confirming, () => null)} />
244 </Box>
245 )}
246 </Box>
247 ))}
248 </Box>
249 )
250 })
251}
252hooks/savepoints.ts 101 lines1import type { SavePoint } from '../types'
2
3// Folders that are rebuilt from scratch or too heavy to copy: never part of a save point.
4export const EXCLUDES = [
5 'node_modules/',
6 '.venv/',
7 'venv/',
8 '__pycache__/',
9 '.next/',
10 '.nuxt/',
11 '.svelte-kit/',
12 '.cache/',
13 'dist/',
14 'build/',
15 'target/',
16 '.DS_Store',
17 '*.log',
18]
19
20const KINDS = ['auto', 'manual', 'restore'] as const
21
22// The save points live outside the project, one git store per folder, so the
23// project's own git (if any) never sees them.
24export function storeDir(home: string, cwd: string): string {
25 const slug = cwd.replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
26 return `${home}/.claude/save-points/${slug || 'root'}`
27}
28
29// Saving the home folder or the disk root would copy everything the person owns.
30export function isUnsafeFolder(cwd: string, home: string | undefined): boolean {
31 const folder = cwd.replace(/\/+$/, '')
32 if (folder === '' || folder === '/') {
33 return true
34 }
35 if (home === undefined) {
36 return false
37 }
38 const homeFolder = home.replace(/\/+$/, '')
39 return folder === homeFolder || homeFolder.startsWith(`${folder}/`)
40}
41
42export function gitArgv(store: string, cwd: string, args: string[]): string[] {
43 return [
44 'git',
45 `--git-dir=${store}`,
46 `--work-tree=${cwd}`,
47 '-c', 'user.name=save-point',
48 '-c', 'user.email=save-point@localhost',
49 '-c', 'commit.gpgsign=false',
50 '-c', 'core.hooksPath=/dev/null',
51 '-c', 'core.quotepath=false',
52 ...args,
53 ]
54}
55
56export function commitMessage(kind: SavePoint['kind'], label: string): string {
57 return `[${kind}] ${label.replace(/\s+/g, ' ').trim()}`
58}
59
60export const LOG_FORMAT = '%H%x09%ct%x09%s'
61
62export function parseLog(stdout: string): SavePoint[] {
63 return stdout
64 .split('\n')
65 .filter(line => line.trim().length > 0)
66 .flatMap(line => {
67 const [sha, seconds, ...rest] = line.split('\t')
68 const subject = rest.join('\t')
69 const match = /^\[(auto|manual|restore)\] (.*)$/.exec(subject)
70 if (sha === undefined || seconds === undefined || match === null) {
71 return []
72 }
73 const kind = KINDS.find(k => k === match[1]) ?? 'auto'
74 return [{ sha, savedAt: Number(seconds) * 1000, kind, label: match[2] ?? '' }]
75 })
76}
77
78// "Prima di: «aggiungi la pagina login»", cut so a long prompt stays one line.
79export function promptLabel(prompt: string, max = 60): string {
80 const oneLine = prompt.replace(/\[(Image|Pasted text) #\d+[^\]]*\]/g, '').replace(/\s+/g, ' ').trim()
81 if (oneLine.length === 0) {
82 return 'Prima che Claude modificasse i file'
83 }
84 const cut = oneLine.length > max ? `${oneLine.slice(0, max - 1).trimEnd()}…` : oneLine
85 return `Prima di: «${cut}»`
86}
87
88export function formatWhen(savedAt: number, now: number): string {
89 const when = new Date(savedAt)
90 const time = `${String(when.getHours()).padStart(2, '0')}:${String(when.getMinutes()).padStart(2, '0')}`
91 const today = new Date(now)
92 const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime()
93 if (savedAt >= startOfToday) {
94 return `oggi ${time}`
95 }
96 if (savedAt >= startOfToday - 86_400_000) {
97 return `ieri ${time}`
98 }
99 return `${when.getDate()}/${when.getMonth() + 1} ${time}`
100}
101types/index.d.ts 19 lines1export type SavePoint = {
2 sha: string
3 /** When it was saved, in milliseconds. */
4 savedAt: number
5 kind: 'auto' | 'manual' | 'restore'
6 label: string
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'save-point': {
12 points: SavePoint[]
13 /** The save point waiting for "Sì, torna qui", if any. */
14 confirming: string | null
15 isBusy: boolean
16 }
17 }
18}
19