Why NextWhy Next
Torna all'elenco
💻 SviluppoLettura di 17 min

Ho smesso di credere al "fatto" dell'agente - prove-it, un cancello su verify.sh

Un agente di coding AI riporta "i test passano" senza averli eseguiti. Non è una bugia, è un problema strutturale. Cosa ho imparato costruendo prove-it, un cancello open source che trasforma il resoconto in un controllo - e i quattro modi in cui un hook di cinque righe scritto a mano fallisce in silenzio.

In sintesi

Un agente di coding non sa distinguere quello che ha fatto da quello che intendeva fare, quindi in pratica riporta le intenzioni. È per questo che dice "i test passano" senza averli lanciati. Non si corregge con i prompt: bisogna trasformare il "fatto" da dichiarazione a controllo da superare. prove-it è un cancello basato su uno Stop hook che impedisce all'agente di chiudere il turno finché il verify.sh alla radice del repo non restituisce exit 0. Un hook di cinque righe scritto a mano fallisce in silenzio in quattro modi - blocca una volta e mai più, lascia passare i turni chiusi con un commit, permette all'agente di cancellare il controllo stesso, oppure rende la resa indistinguibile dal successo. Questi quattro casi sono l'intera ragione per cui prove-it è più lungo di cinque righe.

In questa pagina

"Tutti i test passano. Si può fare il merge."

L'agente ha detto così, e il diff era talmente pulito che stavo per crederci. Poi ho fatto scorrere il terminale verso l'alto: i test non erano mai partiti. Non c'era proprio nessun log di esecuzione. L'agente voleva lanciare i test, e quell'intenzione era finita pari pari nel resoconto di completamento.

Dopo che ti capita qualche volta, la reazione di solito è una di due. Aggiungi al prompt "esegui sempre i test e mostrami il risultato", oppure ricontrolli a mano ogni volta che l'agente dice di aver finito. Le ho provate entrambe, e nessuna delle due è durata. Così ho scelto una terza strada. Invece di rendere inaffidabile la parola "fatto", ho trasformato il completamento da dichiarazione a controllo da superare. Il risultato è uno strumento open source che si chiama prove-it.

Non è una bugia, è struttura

Prima di tutto, un chiarimento. L'agente non sta mentendo.

Una persona distingue "ho lanciato i test" da "volevo lanciare i test", perché ha la memoria. L'agente non ha quel termine di confronto. Non ha modo di verificare quello che ha fatto contro quello che intendeva fare, quindi riporta l'intenzione al posto del risultato. "I test passano" è l'abbreviazione di "ho scritto il codice in modo che i test passino". Non è un difetto di carattere del modello, è una proprietà del suo design, ed è per questo che i prompt non lo correggono. Puoi scrivere "lanciali sempre" cento volte: arriverà il giorno in cui non li lancia.

Le tecniche di prompt hanno anche un altro punto debole: invecchiano a ogni generazione di modelli. Il fatto che un'istruzione funzioni bene con il modello di oggi non garantisce che funzioni con quello di domani. Un meccanismo che esige prove, invece, sta un livello sopra il modello e sopravvive agli upgrade. Se i test sono girati davvero lo dice il codice di uscita, non il modello.

Trasformare il resoconto in un controllo

L'idea in sé - accettare il "fatto" solo come prova, mai come dichiarazione - l'avevo già raccontata in un articolo precedente. Questo articolo racconta come quell'idea è diventata uno strumento che chiunque può installare.

Il contratto di prove-it sta in una frase. Il repo dichiara come dimostrare sé stesso in un file chiamato verify.sh, e l'agente non può rivendicare il completamento finché quella dimostrazione non passa.

L'implementazione funziona così. Metti un verify.sh eseguibile alla radice del repo. Exit 0 significa "questo tree è dimostrabilmente a posto". Quando l'agente prova a chiudere il turno, uno Stop hook esegue lo script e, se il risultato non è 0, invece di lasciarlo fermare lo rimanda a lavorare insieme alle ultime venti righe dell'output di errore. Nella maggior parte dei casi all'agente basta quell'output per correggere la causa.

Anche il momento in cui il cancello scatta è semplice. Questa sessione ha davvero modificato questo repo, esiste un verify.sh eseguibile, e lo stato attuale del tree non è ancora mai passato. Gira solo quando tutte e tre le condizioni sono vere. Le sessioni di sola lettura non vengono toccate, e un tree che è passato una volta non viene ricontrollato.

E non blocca all'infinito. Rimanda indietro al massimo tre volte per turno, poi cede. Un hook che non cede mai pianta la sessione. Ma cedere e passare sono due cose diverse: l'ultimo output di un turno finito con una resa non è "fatto", è l'avviso "questo turno si è chiuso senza verifica". L'agente può anche non riuscire mai a superare il cancello, ma non può superarlo in silenzio - ed è tutto quello che questo strumento promette davvero.

Perché cinque righe di bash non bastano

Se arrivato fin qui hai pensato "ma basta una riga con il comando dei test in uno Stop hook", hai ragione. Bastano cinque righe, e all'inizio l'avevo scritto così anch'io. E quella versione falliva in silenzio in quattro modi. Alcuni erano bug realmente presenti nelle prime versioni di prove-it, quindi oggi ognuno ha il suo test di regressione.

Blocca una volta, e mai più. Dopo il primo blocco, Claude Code imposta il flag stop_hook_active su tutti gli eventi di stop successivi. Un hook che legge quel flag come "lascia passare" smette di essere un cancello dopo aver bloccato esattamente una volta. Al contrario, un hook che ignora il flag blocca per sempre e la sessione si pianta. È una trappola che capisci solo dopo esserci caduto da entrambi i lati, e la risposta è contare da soli i tentativi, bloccare un numero fissato di volte e poi cedere facendo rumore.

Con un commit, sembra che non sia successo niente. Un hook che giudica "c'è stato lavoro?" dallo stato sporco del working tree lascia passare tutti i turni che finiscono con un commit. E il commit è la cosa più normale che un agente faccia. Per questo prove-it registra lo stato del tree all'inizio della sessione come linea di base e lo confronta a ogni stop. Un commit, un file corretto con sed, un file sputato da un generatore di codice: viene catturato tutto come modifica.

L'agente può eliminare il controllo. La mossa più economica per un agente che non riesce a far passare verify.sh è cancellare verify.sh o togliergli il permesso di esecuzione. Il cancello registra all'avvio della sessione se il repo era armato, e rifiuta i turni che finiscono disarmati. C'è anche una mossa più sottile: lasciarlo eseguibile e riscrivere solo i controlli al suo interno. Questa non viene bloccata, perché spesso modificare verify.sh è esattamente il lavoro richiesto. Ma non passa nemmeno in silenzio: se il turno passa con un verify.sh cambiato durante la sessione, il cancello te lo segnala, e leggendo quel diff puoi giudicare se era lavoro o evasione.

La resa è indistinguibile dal successo. Qualunque host, prima o poi, costringe il hook a cedere. Un hook scritto a mano cede in silenzio, e l'ultima cosa che vedi è "fatto". Un turno che ha fallito tre volte e si è arreso e un turno passato al primo colpo sono identici sullo schermo. L'ultima parola di prove-it è un avviso. Se la differenza ti sembra un dettaglio, immagina il giorno dopo aver fatto il merge di un turno arreso credendolo passato.

Questi quattro casi sono l'intera ragione per cui prove-it è più lungo di cinque righe. Se vuoi continuare a usare il tuo hook, va benissimo. Ma i casi che quel hook deve gestire sono scritti in SPEC.md, e ti consiglio di leggerli. Quello che conta è il contratto, non l'implementazione.

Cosa questo strumento non dimostra

C'è un limite da mettere nero su bianco, per onestà. Il cancello impone una sola cosa: che verify.sh abbia restituito 0 prima della fine del turno. Cosa significhi quello 0 dipende interamente dai controlli che ci hai scritto tu. Un verify.sh fatto di una sola riga exit 0 supera questo cancello e non dimostra niente.

La spec lo chiama Level 1. Il Level 2 è la questione se i controlli facciano asserzioni su prove reali, e quello nessuno strumento può verificarlo al posto tuo. Nemmeno prove-it. Agganciare il hook è la parte facile. Il vero lavoro è rispondere alla domanda "cosa significa dimostrato in questo repo", e la risposta cambia da repo a repo. Per questo il contratto fissa solo il nome del file, non il contenuto.

Anche le vie di fuga sono lasciate aperte di proposito. Con PROVE_IT_SKIP=1 il turno passa e basta, e se cancelli verify.sh il cancello si spegne per sempre. Un cancello che non si può rimuovere è un cancello che la gente prima o poi aggira, e un cancello aggirato è peggio di nessun cancello: riporta i controlli come eseguiti mentre in realtà non gira niente.

Il primo giorno, volutamente piccolo

L'installazione è di tre righe, e il lavoro vero lo fa la terza.

/plugin marketplace add Why-Next/prove-it
/plugin install prove-it@whynext
/prove-it:init

/prove-it:init rileva lo stack, crea il verify.sh, ti mostra sotto gli occhi che passa, e poi esegue una copia con un exit 1 in fondo per farti vedere che il cancello rifiuta il turno. Ci vogliono una trentina di secondi.

L'unico controllo attivo nel verify.sh generato è git diff --check. Test e type check sono tutti solo commentati. È fatto apposta perché passi su main il giorno stesso in cui lo installi. Un cancello che fallisce dal primo giorno insegna al team ad aggirarlo dalla prima settimana. I controlli commentati si attivano uno alla volta, dopo averli lanciati a mano e averli visti passare. E prima di attivarne uno, bisogna farlo fallire di proposito almeno una volta. Un controllo che non può fallire non è un controllo, e scoprirlo il giorno in cui ti serve è troppo tardi.

Anche il punto in cui smettere di far crescere verify.sh è fissato: un minuto in totale. I controlli più lenti vanno in CI. Anche quelli che richiedono secrets o accesso alla produzione sono roba da CI. In verify.sh restano solo le prove che l'agente può produrre in locale mentre lavora.

Il registro di quello che il cancello ha catturato

Con PROVE_IT_LEDGER=1 attivo, ogni volta che il cancello cattura un falso completamento si accumula una riga in un file locale.

{"ts":"2026-07-09T04:12:33Z","claim":"All tests pass. Ready to merge.",
 "evidence_demanded":"verify.sh exit 0","actual":["3 failed, 41 passed"]}

Cosa ha rivendicato l'agente, cosa gli è stato chiesto, e com'era davvero. Dopo un mese di righe accumulate, il modo in cui il tuo agente fallisce lo leggi nei dati invece di indovinarlo. Il file resta solo sul disco locale, non viene trasmesso da nessuna parte, ed è spento finché non lo attivi.

Questo repo si mette il cancello da solo

Ovviamente anche il repo di prove-it ha il suo verify.sh, e lì dentro il cancello testa sé stesso sopra veri repo git. Un controllo che fallisce blocca? Uno che passa apre? Le sessioni di sola lettura restano intatte? Il tree pulito dopo un commit non viene scambiato per "nessun lavoro"? Le quattro trappole di cui sopra sono tutte inchiodate come test di regressione.

Licenza MIT, e le dipendenze sono solo bash, git e python3. È testato con Claude Code, e il modo di collegarlo a host che espongono lo stesso tipo di hook bloccante - Codex CLI, Gemini CLI e simili - è nel documento ADAPTERS. Se hai un repo dove l'agente modifica il codice e nella stessa conversazione dice "fatto", quello è il posto dove questo cancello serve di più.

"Fatto" non dovrebbe essere una parola che l'agente dichiara, ma uno stato che il repo giudica. Scrivere i criteri di quel giudizio resta compito tuo, ma almeno adesso nessuno può saltare il giudizio e passare in silenzio.

Domande frequenti

Il mio agente di coding AI riporta come completato un lavoro che non ha mai fatto. Come lo fermo?

Scrivere nel prompt "esegui sempre i test" non basta. L'agente non ha modo di confrontare ciò che ha fatto con ciò che intendeva fare, quindi riporta le intenzioni così come sono. Trasforma invece la dichiarazione di completamento in un controllo: metti uno script di verifica (verify.sh) alla radice del repo e fai in modo che, quando l'agente prova a chiudere il turno, uno Stop hook esegua quello script e lo rimandi a lavorare se il risultato non è exit 0. prove-it è l'implementazione di riferimento di questo contratto.

Non posso scrivermi da solo uno script di cinque righe che lancia i test in uno Stop hook di Claude Code?

Puoi, ma ci sono quattro trappole. ① Dopo il primo blocco Claude Code imposta il flag stop_hook_active: se lo gestisci male, blocchi una sola volta oppure blocchi per sempre e la sessione si pianta. ② Se giudichi dal working tree sporco, lasci passare tutti i turni che finiscono con un commit. ③ L'agente può disarmare il cancello cancellando verify.sh o facendogli chmod -x. ④ Quando il hook alla fine cede, la resa sembra un successo. Se vuoi scriverlo da solo, devi gestire tutti e quattro i casi.

Quali controlli devo mettere in verify.sh?

La risposta alla domanda "cosa deve essere vero perché una modifica sia dimostrata in questo repo". Di solito si attivano uno alla volta: il comando dei test che lanci davvero, il type check, il lint. Il totale deve finire entro un minuto; i controlli che richiedono secrets o accesso alla produzione vanno in CI. L'importante è non essere ambiziosi dal primo giorno: un cancello lento o instabile viene aggirato entro una settimana, e un cancello aggirato è peggio di nessun cancello.

Articoli correlati

0 commenti