Modello di sicurezza
aipager esegue Claude Code per conto tuo e si comanda da Telegram. Da qui nascono due domande:
- Chi può comandare il bot?
- Quando viene comandato, cosa può fare?
Confine di fiducia#
Ogni gestore che il bot registra (messaggi, file, vocali, pulsanti) passa dal filtro delle chat di python-telegram-bot, costruito con le chat configurate in ~/.config/aipager/aipager.yaml, oppure (per i comandi) dallo stesso controllo nella fase di autorizzazione di aipager. L'unica eccezione registra soltanto i mittenti nei gruppi non configurati (vedi sotto). Solo le chat configurate possono interagire con il bot. Niente che arrivi da altre chat viene elaborato o passato a una sessione. Una chat privata non configurata riceve un'unica risposta, «Questo bot non è configurato per parlare con te». In un gruppo non configurato il bot non risponde mai, ma registra chi gli ha scritto lì (l'ID di Telegram, lo username e il nome della persona, più l'ID, il titolo e il tipo del gruppo) in ~/.claude/aipager-pending-users.json, così aipager config può trovare un nuovo gruppo e le sue persone mentre aipager è in esecuzione. Chiunque può aggiungere il bot a un gruppo, quindi chiunque può aggiungere una riga a questo file; questo non gli dà nessun accesso. Il file lo puoi leggere solo tu (permessi 0600), viene sostituito per intero a ogni scrittura (mai lasciato a metà) e tiene al massimo le 200 persone più recenti, togliendo chi non si fa vedere da 30 giorni. Nell'uso in team, sopra c'è anche un elenco di persone ammesse con i loro ruoli, e in un gruppo vengono elaborati solo i messaggi rivolti al bot (un comando, una risposta a un messaggio del bot, una menzione del bot o un tocco sulla tastiera del bot), che il bot sia admin del gruppo oppure no (vedi gruppi).
Quindi la superficie aperta al mondo esterno è:
- Il token del bot (un segreto).
- L'ID della chat (un intero lungo).
Se trapelano entrambi, un attaccante può comandare il tuo aipager. Se trapela solo il token, un attaccante può leggere i messaggi inviati al bot dalla tua chat ma non può inviare comandi che aipager eseguirebbe. Se trapela solo l'ID della chat, non succede niente di serio: il bot ha bisogno del token per parlare con Telegram.
Segreti#
| Segreto | Dove | Permessi |
|---|---|---|
| Token del bot | ~/.config/aipager/aipager.yaml | 600 (predefinito) |
| ID della chat | stesso file | 600 (predefinito) |
Il token non compare nel log di aipager: le righe delle richieste HTTP che lo contengono non vengono registrate, e i messaggi di errore, aipager doctor e la procedura guidata nascondono tutto ciò che sembra un token di bot. L'ID della chat compare nel log di aipager (per esempio nelle righe sui limiti di frequenza), quindi tratta l'output di aipager logs come privato. Nessuno dei due finisce in un commit: la configurazione sta nella ~/.config dell'utente, non nel repo. Anche il processo di pubblicazione su PyPI tramite Trusted Publisher non tocca mai i segreti; l'autenticazione la gestisce OIDC.
Se pensi che il token sia trapelato, revocalo da @BotFather (/revoke), creane uno nuovo ed esegui di nuovo aipager config.
La credenziale di Claude: cosa la protegge davvero#
aipager esegue un agente autonomo con accesso a Bash come il tuo utente UNIX. Quindi qualsiasi segreto che aipager può leggere, lo può leggere anche l'agente che avvia. Vale la pena dirlo chiaramente, perché è facile affidarsi al tipo sbagliato di protezione.
~/.config/aipager/ è nell'elenco dei divieti di safety.py, che impedisce a una sessione comandata da Telegram di leggerne i file. È una protezione per percorso e qui non serve: la credenziale di Claude (CLAUDE_CODE_OAUTH_TOKEN o ANTHROPIC_API_KEY) non si ottiene tramite un percorso, viene passata nell'ambiente del processo di ogni sessione avviata. Dentro una sessione, echo $CLAUDE_CODE_OAUTH_TOKEN la stampa, con o senza elenco dei divieti. Una regola sui percorsi dei file è lo strumento sbagliato per una variabile d'ambiente.
Dove sta la credenziale#
| Installazione | Origine | Permessi |
|---|---|---|
| Servizio utente di systemd | LoadCredential=claude_oauth:%h/.config/aipager/daemon.env, letto da $CREDENTIALS_DIRECTORY/claude_oauth | 600 (daemon.env); systemd tiene la credenziale fuori dall'ambiente del servizio stesso |
| macOS / Docker / senza systemd | ~/.config/aipager/daemon.env, letto direttamente | 600 |
Entrambe le strade sono lette dallo stesso codice (aipager/daemon_secrets.py) e passate alla sessione avviata tramite la tabella env= del processo figlio, mai inserite in una stringa di comando bash -c. /proc/PID/cmdline lo può leggere chiunque (0444); /proc/PID/environ è 0400 (solo il proprietario).
Cosa danno davvero LoadCredential= più 0600: gli altri utenti UNIX dello stesso computer non possono leggere la credenziale, e non compare mai in systemctl show, in una copia di sicurezza del file di servizio, o in uno screenshot dell'output di journalctl che il proprietario del bot incolla in una segnalazione di bug. Non crea nessun confine tra aipager e l'agente che aipager avvia: quel confine non esiste e nessuna regolazione dei permessi dei file può crearlo, perché il lavoro stesso dell'agente è agire come il proprietario del bot dentro quell'ambiente.
L'unica misura che funziona davvero#
Limita la credenziale stessa. Fai girare aipager con un account Anthropic separato o con una chiave API che abbia un limite di spesa, non con lo stesso token che usi per il tuo accesso interattivo a claude. Revocare la credenziale di aipager non deve mai chiudere il tuo accesso personale; se succede, sono la stessa credenziale e nessuna delle due è limitata. aipager doctor --fix può trovare un token esistente da mettere in daemon.env, ma quale token dargli lo decide il proprietario del bot, ed è l'unica decisione di questo documento che conta più dei permessi dei file che lo circondano.
Diagnosticare l'accesso senza tirare a indovinare#
aipager doctor (e l'avviso di una riga inviato una volta a ogni avvio di aipager) riferisce lo stato dell'accesso tramite il claude auth status di Claude Code, mai con un controllo dei file fatto in casa. I tre stati si scrivono sempre in modo diverso, ovunque compaiano:
auth: <method> (<source>): accesso confermato.auth: none (not logged in): confermato che l'accesso non c'è.auth: unknown (...): la prova stessa è fallita (timeout, programma mancante, output illeggibile) oppure il programma è più vecchio della versione che ha aggiunto l'output JSON. Questo caso non viene mai riferito come «accesso non fatto»: aipager non rifiuta di avviare una sessione per un controllo dell'accesso di cui non è sicuro. L'accesso tramite il Portachiavi di macOS, credenziali con solo un refresh token e gli accessi con piano Max senza file sono invisibili a qualsiasi controllo dei file e altrimenti sembrerebbero identici a un accesso mai fatto.
Chi decide se una chiamata a uno strumento viene eseguita#
Due controlli separati decidono se una chiamata a uno strumento viene eseguita, e girano in quest'ordine:
- Il controllo di sicurezza di aipager. Claude Code esegue l'hook
PreToolUsedi aipager prima di ogni chiamata a uno strumento. Per un'attività partita da Telegram, l'hook applica alla chiamata le regole del ruolo di chi l'ha inviata e la base di sicurezza (percorsi protetti, schemi di comandi) e nega quello che non permettono: allora Claude Code nega lo strumento senza chiedere a nessuno, qualunque cosa dicano le sue regole. Un messaggio scritto nel terminale non ha limiti da parte di questo controllo. Comprende quanto descritto più sotto in applicazione delle regole nell'uso in team. - Le regole di Claude Code (
~/.claude/settings.json, la modalità dei permessi della sessione). Se dicono di chiedere, Claude Code emettePermissionRequeste aipager mostra la richiesta di permesso in Telegram (vedi hook). Il tuo tocco torna a Claude Code come decisione dell'hook, oppure, se i 20 secondi di attesa sono passati, viene scritto nel dialogo di Claude Code. Se le regole di Claude Code negano la chiamata, non arriva mai a Telegram.
Quindi aipager può negare una chiamata che Claude Code consentirebbe e, quando Claude Code chiede, aipager non risponde mai sì da solo: un consenso è sempre il tocco di una persona. Tre cose fanno eseguire chiamate senza chiedere:
- Modalità «Completo», per gli admin (e per una persona nella propria chat privata). Una sessione in modalità «Completo» parte con
--dangerously-skip-permissions: Claude Code non chiede niente, quindi ogni chiamata che il controllo di sicurezza di aipager non nega viene eseguita senza un tocco. Solo un admin (i ruoliownereadmin, o un ruolo concan_manage) può avviare una sessione in modalità «Completo», passarci una sessione (/mode full, dopo una conferma) o riprenderla così, con un'eccezione: nella propria chat privata con il bot, una persona il cui ruolo può inviare messaggi e rispondere a Claude (can_promptecan_approve, cioèuserma nonread_only) può fare lo stesso per le proprie sessioni (/new !x1, o «Nuove sessioni» impostato su «Completo» in quella chat,/mode full,/resume), in Telegram e nella Mini App. Mai in un gruppo né nella chat privata di qualcun altro. Tutti gli altri ricevono invece la modalità «Automatico», e la risposta lo dice. La modalità «Completo» salta solo le domande di Claude Code: l'hook PreToolUse di aipager applica comunque le regole del ruolo della persona (base di sicurezza, limiti delle cartelle, regole di divieto, file con le credenziali) a ogni chiamata, esattamente come nelle modalità «Chiedi» e «Automatico»; la modalità riferita da Claude Code non le allenta mai. Il controllo di sicurezza di aipager gira comunque a ogni chiamata in modalità «Completo», ma non nega niente nelle attività del proprietario stesso o in un messaggio scritto nel terminale. - Modalità «Automatico», la predefinita. Una sessione in modalità «Automatico» parte con
--permission-mode auto: il controllo di sicurezza di Claude Code esamina ogni chiamata, esegue quelle sicure e ferma quelle rischiose, che arrivano in Telegram come richieste di permesso, come nella modalità «Chiedi». Ogni sessione parte in modalità «Automatico», a meno che la chat non abbia scelto altro in/settings→ Nuove sessioni. Chiunque possa agire su una sessione può passarla tra «Chiedi» e «Automatico» (/mode); aipager lo fa sul posto premendo Shift+Tab nella sessione, mai con un'opzione che potrebbe aggiungere «Completo» al giro di Shift+Tab di Claude Code. Se un giorno Claude Code riferisce la modalità «Completo» per una sessione che aipager non ha avviato così, aipager mostra quello che è stato riferito, scrive un avviso nel log e non avvia né riavvia mai per questo una sessione in modalità «Completo». Limite noto: la modalità riferita da Claude Code arriva dal socket dell'hook, su cui può scrivere qualsiasi processo del computer (vedi connessioni di rete). Un falso rapporto può cambiare la modalità con cui viene mostrata una sessione e, mentre un passaggio tra «Chiedi» e «Automatico» aspetta conferma, può far premere ad aipager Shift+Tab una volta in più, lasciando la sessione in esecuzione in «Automatico» quando era stata chiesta «Chiedi» (il prossimo hook vero mostra di nuovo la modalità reale). Non cambia mai l'opzione che passano un riavvio o una ripresa e non può mai arrivare alla modalità «Completo»: solo l'opzione di avvio la imposta. aipager non preme Shift+Tab in una sessione che sa poter girare fino a «Completo»: una sessione avviata in «Completo» (anche dopo che è stata passata a «Chiedi» o «Automatico» nel terminale), o una che ha riferito «Completo» all'aipager in esecuzione da quando è partito. Lì ogni cambio di modalità è un riavvio che passa l'opzione della nuova modalità e toglie quella di «Completo», e un falso rapporto di «Completo» può al massimo trasformare un passaggio in un riavvio. aipager non lo può sapere per una sessione avviata nel terminale con l'opzione che aggiunge «Completo» al giro di Shift+Tab (--allow-prima dell'opzione di «Completo»), o con--dangerously-skip-permissionsquando ha lasciato «Completo» prima che aipager ne avesse notizia (aipager non era in esecuzione, o da allora si è riavviato). Lì un passaggio da «Chiedi» ad «Automatico» può fermarsi su «Completo», e allora la scheda dice che la sessione è in modalità «Completo». Non usare quelle opzioni per sessioni del terminale di cui chi non è admin può cambiare la modalità da Telegram. Perché la modalità «Completo» si possa usare da Telegram, aipager impostaskipDangerousModePermissionPromptin~/.claude/settings.jsona ogni avvio (altrimenti l'avviso di Claude Code prima di girare in questa modalità bloccherebbe la sessione); questo toglie l'avviso anche per le sessioni che avvii tu. Allo stesso modo segna come fidata in~/.claude.jsonla cartella in cui parte ogni sessione (a meno che quella cartella o una sopra sia già fidata), così la domanda «Do you trust this folder?» di Claude Code non blocca una sessione avviata da Telegram: il primo messaggio risponderebbe «No, exit». Le impostazioni.claude/, gli hook di.git/e il.mcp.jsondi una cartella fidata possono eseguire comandi; per questo i membri senzacan_managenon possono mai scrivere quei file (a meno chepolicy.yamlnon ridia una shell al loro ruolo). Claude Code estende la fiducia di una cartella alle cartelle sotto che non sono dentro un repository git; un repository riceve comunque la sua domanda. La cartella home viene segnata quando una sessione parte lì, perché Claude Code tiene un sì dato nella cartella home solo per quella sessione. Da lì in poi Claude Code non chiede più niente in una cartella sotto la tua cartella home che non sia dentro un repository git, anche nelle sessioni che avvii tu./e le cartelle sopra la home non vengono mai segnate: arriverebbe a tutte quelle cartelle del computer, quindi una sessione avviata in una di esse si ferma alla domanda. All'avvio aipager non segna niente, solo la cartella in cui una sessione sta per partire. Una nuova sessione parte nella cartella in cui gira aipager (la cartella home conaipager service installsu Linux e macOS, e nel container) o inAIPAGER_WORK_DIRse è impostata; il log di aipager avvisa all'avvio quando quella cartella è/o una cartella sopra la home e non è fidata. Le versioni prima di questa segnavano la cartella di aipager a ogni avvio,/compresa per un servizio macOS (che girava in/); aipager non può distinguere una voce così da una creata da te, quindi la lascia. aipager non tocca~/.claude.jsone~/.claude/settings.jsonquando non riesce a leggerli per intero, o quando un Claude in esecuzione li ha cambiati mentre aipager li aggiornava. - Consenti sempre, che restituisce a Claude Code la regola «non chiedere più» che ha offerto con la richiesta, esattamente come quella scelta nel suo dialogo.
Per bloccare uno strumento a tutti, negalo in ~/.claude/settings.json; per bloccarlo ad alcune persone, usa le regole del loro ruolo in policy.yaml (vedi gruppi).
Quello che mostra una richiesta di permesso è quello che verrà eseguito. Una richiesta di permesso mostra il comando o il percorso vero sotto la descrizione di Claude. I caratteri di controllo della direzione di Unicode (U+202A-U+202E, U+2066-U+2069, U+200E, U+200F, U+061C) cambiano l'ordine in cui Telegram, un browser o un telefono disegnano il testo che viene dopo, mentre la shell esegue i caratteri nel loro ordine, quindi possono far sembrare un comando un altro (il trucco «Trojan Source», CVE-2021-42574). In ogni richiesta di permesso, domanda, anteprima delle modifiche e voce di audit vengono mostrati come segni visibili (⟨U+202E⟩), e la richiesta di permesso avvisa che il comando potrebbe non fare quello che sembra. Gli altri caratteri nascosti restano come sono.
Applicazione delle regole nell'uso in team#
Nell'uso in team aipager aggiunge un livello suo sotto i tocchi: ogni messaggio da Telegram porta una nota di permessi (chi l'ha inviato e cosa permette il suo ruolo), scritta in /tmp/claude-notes-<session>/, un file per messaggio. Quando Claude prende un messaggio, il programma di supporto aipager-hook lo confronta con quelle note e mette le regole del mittente dove le legge il controllo PreToolUse. Tutto il resto poggia su queste proprietà:
- Vince la più restrittiva. Se Claude prende messaggi di più persone nella stessa attività (aipager trattiene i messaggi per evitarlo, ma resta sicuro anche se succede), quell'attività gira con la combinazione più restrittiva: il messaggio del proprietario non presta mai i suoi permessi a nessun altro.
- Un'attività in corso non si allarga mai. Il messaggio di un'altra persona aspetta che l'attività in corso finisca, poi gira come attività a sé con le regole di chi l'ha inviato. Se un messaggio si unisce comunque a un'attività in corso, quell'attività tiene le regole più restrittive di tutti quelli che ci sono dentro; un messaggio di Telegram che si unisce a un'attività che hai scritto nel terminale applica al resto di quell'attività le regole di chi l'ha inviato (il messaggio del proprietario non cambia niente).
- Il tuo ruolo è quello della chat in cui agisci. Una persona presente in più chat (diciamo
userin un gruppo eownernella propria chat privata) ha il ruolo del gruppo per tutto quello che invia o tocca nel gruppo, e il ruolo della chat privata nella chat privata, qualunque sia l'ordine delle chat inaipager.yaml. - Nel dubbio, limitato. Un messaggio di cui non si sa chi l'ha inviato gira con la base predefinita (nessun permesso di saltare le regole, tutte le regole di divieto valide), mai come un messaggio del terminale senza limiti. Un messaggio che scrivi tu direttamente nel terminale resta senza limiti; tutto ciò che porta il segno di Telegram su qualsiasi riga viene controllato. Ogni messaggio di Telegram porta quel segno, con il nome di chi ha inviato proprio quel messaggio (mai di chi ha guidato la sessione per ultimo); un mittente che aipager non conosce, per esempio qualcuno tolto da
aipager.yamlmentre il suo messaggio aspettava, riceve un segno[via Telegram]senza nome e la base. - I comandi slash richiedono un admin. Un comando slash viene scritto a Claude così com'è, senza nessun segno, e un comando o una skill come
/deliverfa girare un'intera attività a partire dal proprio contenuto. Per questo un membro il cui ruolo non può saltare le regole del ruolo (i predefinitiusereread_only, i ruoli che definisci tu senzabypass_role_denies, e chiunque aipager non conosca) può inviare solo i comandi della tastiera «Comandi» di aipager (più/compact) e i cambi di modello (/model <name>); qualsiasi altro comando slash, scritto, inviato a una sessione con/<name>, come didascalia di un file, come primo messaggio di/newo come template, viene rifiutato con «Quel comando è solo per gli admin.» e un 🤷. Proprietario e admin possono inviare qualsiasi comando. Un comando slash che arriva comunque a Claude da Telegram viene controllato: l'hook vede che l'attività ha preso un messaggio di Telegram che ha inviato quel comando e la sottopone alle regole del mittente. Un comando slash che scrivi tu nel terminale resta senza limiti, a meno che lo stesso comando (con qualsiasi argomento) non sia già stato inviato da Telegram prima in quella sessione: aipager non distingue la tua copia da Claude Code che rimanda quella di Telegram, quindi allora la tua gira con le regole dell'ultima attività di Telegram. Questo finisce con la fine della sessione. - Un messaggio senza un nuovo mittente mantiene le regole dell'attività. Mentre un'attività da Telegram è in corso, Claude Code riferisce anche messaggi che nessun messaggio di Telegram spiega: un messaggio scritto nel terminale e messo in coda dietro quell'attività (riferito nel momento in cui va in coda), un messaggio da un'altra sessione di Claude sul computer, oppure lo stesso messaggio di Telegram reinviato dopo una compattazione. Un messaggio così non aggiunge nessun mittente di Telegram, quindi l'attività mantiene le regole con cui gira, ristrette da qualsiasi messaggio ancora in attesa di essere preso e mai allentate. Un messaggio con un testo di Telegram che aipager non sa attribuire ricade comunque sulla base predefinita, e un messaggio di un mittente con meno permessi abbassa comunque l'attività alle regole di quel mittente.
Cosa trattiene davvero un utente limitato. La sessione di Claude gira come lo stesso utente del sistema che possiede la configurazione di aipager (il token del bot in ~/.config/aipager/aipager.yaml, la credenziale di Claude in ~/.config/aipager/daemon.env) e il suo file di regole per attività (/tmp/claude-policy-<session>.json). Una shell che gira come quell'utente può raggiungere entrambi con scritture che nessuno schema prevede (sed -i … /tmp/*-policy-*.json, un ciclo for su un glob, una riga di Python) e riscrivere le proprie regole in quelle del proprietario. Quindi gli schemi di comandi sono un filtro, non un confine. Per un'attività che gira con i ruoli limitati predefiniti (user, read_only), e per un'attività che aipager non sa di chi sia, aipager applica queste regole, e nessuna dipende dagli schemi di comandi (vedi i limiti noti più sotto per cosa non coprono):
- Niente shell. Non possono usare
Bash, né nessun altro strumento con cui Claude Code esegue codice (PowerShell,Monitor,REPL,Workflow, i programmatori di messaggiCronCreateeRemoteTrigger, …; l'elenco èsafety.CODE_EXECUTION_TOOLS). Un messaggio programmato arriva senza segno di Telegram, cioè senza limiti, quindi anche quegli strumenti contano. Per la stessa ragione non possono usareSendMessage(può passare un messaggio a un'altra sessione di Claude sul computer, dove girerebbe come un messaggio del terminale), néEnterWorktree/ExitWorktree(cambiano la cartella di lavoro della sessione, su cui poggia la regola successiva). - Le scritture di file restano nel progetto.
Write,Edit,MultiEditeNotebookEditscrivono solo dentro la cartella della sessione (la cartella di lavoro che Claude Code riferisce nei dati dell'hook, mai un percorso preso dalla chiamata allo strumento) e nello scratchpad di Claude Code della sessione (/tmp/claude-<uid>/<project>/<session id>/, o/tmp/claude-<uid>/quando non si può determinare). Tutto il resto viene negato: i file di avvio della tua shell,~/.ssh, altri progetti, i file di aipager in/tmp. Dentro la cartella vengono negati anche.claude/,.git/e.mcp.json, perché ognuno di questi fa eseguire un comando a Claude Code o a git. I link simbolici vengono seguiti prima di decidere. Una sessione la cui cartella è la tua cartella home,/o una cartella sopra la home non ha una cartella di progetto: lì un'attività limitata scrive solo nel suo scratchpad, e una scrittura altrove viene negata con «questa sessione lavora nella cartella home (o in una cartella sopra) e questo ruolo può scrivere solo dentro una cartella di progetto». Perché i membri limitati non restino bloccati lì,/newe la Mini App non li fanno partire in quelle cartelle (la cartella di aipager, predefinita per una nuova sessione, è la tua cartella home quando gira come servizio in background); possono creare una nuova cartella dentro la tua cartella home e partire da lì (deve essere nuova: non una che hai già lì, e nonbinonode_modules, da cui altri programmi caricano codice). - I membri restano nella loro cartella radice. Un ruolo senza
can_manage(i predefinitiusereread_only) lavora dentro la cartella radice della chat, che imposti inaipager config(predefinita~/aipager-users/<label>;~/aipager-userse la cartella vengono create con permessi 0700)./new, Riprendi, Sostituisci, Riavvia, il cambio di modalità e la Mini App avviano o riavviano le sessioni di un membro così solo dentro quella cartella. L'hook di sicurezza lo fa rispettare qualunque sia la strada da cui arriva un messaggio (Telegram, la Mini App, un messaggio da un'altra sessione): la cartella radice viaggia con l'istantanea delle regole dell'attività, con la ricevuta di un messaggio tra sessioni e con i permessi fissati o programmati, e quando membri con cartelle radice diverse condividono un'attività vince la più stretta (due cartelle che non si contengono lasciano solo la cartella della sessione). Le scritture vanno nella cartella della sessione solo quando quella cartella è dentro la cartella radice (una sessione fuori, per esempio una avviata prima che esistessero le cartelle radice, continua a girare, ma le sue attività scrivono solo nello scratchpad: «questa sessione lavora fuori dalla cartella di questo ruolo»), eRead,LSP,GrepeGlobnon toccano niente nella tua cartella home fuori dalla cartella radice («nella cartella home, questo ruolo legge solo dentro la propria cartella»), tranne lo scratchpad della sessione e il suo file di risposta. La cartella radice di ogni altra chat è un percorso protetto per quel membro, ovunque si trovi, e lo è anche la cartella dei file caricati di ogni altra chat sotto/tmp/aipager-files/(Read,Grep,Globe gli strumenti di scrittura; per un ruolo a cui è stato dato Bash, un comando che ne nomina una viene rifiutato, al meglio delle possibilità), insieme ai file che tutte le chat condividevano lì prima che esistessero le cartelle per chat. I file caricati nella chat del membro stesso restano leggibili. Le cartelle fuori dalla tua cartella home (/usr,/etc,/srv) restano leggibili. Un ruolo personalizzato conbypass_role_deniesobypass_safetyma senzacan_managescrive e cerca ovunque dentro la sua cartella radice, e anche le sue letture nella tua cartella home restano lì dentro: solo proprietario e admin non hanno limiti. Conbypass_safety, in una sessione la cui cartella è fuori dalla sua cartella radice, scrive e cerca solo nello scratchpad, come unuser, e quando una sua chiamata a uno strumento viene negata il resto dell'attività si ferma. - Le ricerche restano nel progetto. Un
Grepo unGlobè consentito solo quando è vero tutto questo: la cartella in cui cerca (il suopath, o la cartella della sessione se non c'è, con i link simbolici seguiti) è dentro la cartella della sessione (che non conta quando è la tua cartella home,/o una cartella sopra la home, come per le scritture) o nello scratchpad; quella cartella non è un percorso protetto e non ne contiene uno (una sessione avviata nella tua cartella home contiene~/.config/aipager, quindi non si può cercare dalla sua radice); ogni glob è un semplice schema relativo (niente/,~,$o lettera di unità all'inizio, niente.., niente#(un commento per ripgrep) o!(negazione), niente escape\); e non hai regole di percorsi protetti che non comincino con/o~(**/.envpotrebbe stare ovunque nel progetto, quindi con una regola così ogni ricerca limitata viene negata). Tutto il resto viene negato. È voluto che sia un elenco di cose permesse: tre revisioni hanno trovato ognuna una nuova scrittura di glob che portava ripgrep fuori dalla cartella prevista da un modello di come la interpreta, e una di queste leggeva il token del bot. - Gli strumenti sui file rispettano i percorsi protetti.
Read,LSPe gli strumenti di scrittura controllano dove porta davvero un percorso, anche attraverso i link simbolici. L'unico file di controllo che un'attività può leggere è il/tmp/claude-reply-<session>.txtdella sua sessione, quello che aipager nomina nel messaggio quando rispondi a un messaggio precedente; i file di risposta delle altre sessioni, e qualsiasi scrittura nel proprio, restano negati. Su macOS/tmpè/private/tmpe il disco non distingue maiuscole e minuscole, quindi vengono riconosciute entrambe le forme e ogni combinazione di maiuscole. - Un controllo che si rompe nega. Se la decisione su una chiamata a uno strumento genera un errore (un byte NUL in un percorso, un bug), o l'hook finisce la memoria mentre decide, la chiamata viene negata, a meno che le regole della sessione si possano leggere e concedano al proprietario di saltarle. Prima veniva lasciata passare.
I tuoi percorsi protetti. La sezione safety: di policy.yaml (deny_paths_no_access, deny_paths_no_write, deny_bash_patterns) si aggiunge ai percorsi protetti e agli schemi di comandi predefiniti. Fa parte della base che ricevono tutte le attività di tutti i ruoli tranne owner, admin compresi, e vale per gli strumenti sui file, le ricerche e i comandi Bash proprio come l'elenco predefinito. aipager scrive l'elenco unito in /tmp/claude-policy-.floor-<uid>.json (anche questo un percorso protetto) per i percorsi di riserva dell'hook di sicurezza; se quel file manca, non si legge o è di un altro utente del sistema, l'hook usa l'elenco predefinito, mai di meno. Una modifica fatta con una ricarica senza riavvio vale per i messaggi inviati dopo la ricarica, non per un'attività in corso o per un messaggio in attesa. Fino alla 0.7.20 questa sezione veniva letta e mostrata da aipager doctor --safety-check ma non applicata.
Attività che aipager non sa di chi siano. Un'attività da Telegram senza un mittente a cui attenersi gira con la base, che corrisponde al ruolo predefinito user (senza le restrizioni in più che hai aggiunto per user in policy.yaml) più la tua sezione safety:: niente strumenti che eseguono codice, scritture e ricerche limitate, percorsi protetti. Succede per un messaggio la cui nota di permessi è scaduta (dopo 24 ore) o non si è potuta scrivere, e per un messaggio con un testo di Telegram che nessuna nota spiega. Il pulsante Riprova gira con i permessi di chi lo tocca quando è stata quella stessa persona a inviare il messaggio da riprovare, quindi il Riprova del proprietario sul proprio messaggio mantiene Bash; un Riprova sul messaggio di qualcun altro, o su uno di cui aipager non conosce il mittente, gira con la base, così il testo di nessuno può prendere in prestito i permessi di chi tocca.
Le letture fuori dai percorsi protetti sono permesse per un ruolo che non è legato a una cartella radice (un ruolo personalizzato con can_manage ma senza permessi per saltare le regole): può leggere gli altri progetti di quell'utente del sistema. I predefiniti user e read_only non leggono niente nella tua cartella home fuori dalla loro cartella radice (sopra). I file con le tue credenziali sono percorsi protetti per ogni ruolo senza bypass_role_denies (i predefiniti user e read_only, i ruoli che definisci tu e la base): ~/.ssh, ~/.gnupg, ~/.aws, ~/.config/gh, ~/.git-credentials, ~/.netrc, ~/.docker/config.json, ~/.kube, ~/.config/gcloud, ~/.azure, ~/.password-store, ~/.pgpass, ~/.npmrc e ~/.pypirc non si possono leggere, scrivere o cercare. Sono il valore predefinito del ruolo, quindi il deny_paths_no_access del ruolo in policy.yaml li sostituisce per quel ruolo, tranne quando a un'attività in corso si unisce un altro messaggio o un messaggio scritto nel terminale: da quel momento fino alla fine dell'attività torna a essere legata a questo elenco (vince la regola più restrittiva). Un file con credenziali altrove (un .env nel progetto, un token in un'altra cartella) non è coperto: aggiungilo al deny_paths_no_access del ruolo. Il proprietario non è legato a questi. Le scritture di un admin non hanno limiti. Il suo Grep/Glob può cercare in qualsiasi cartella che non tocchi un percorso protetto: una ricerca viene negata quando la cartella in cui cerca (il suo path, o la cartella della sessione se non c'è; per Glob, anche la cartella che nomina il suo schema, come in /home/you/.config/aipager/*, o quella sopra uno schema senza caratteri jolly, dove Claude Code cerca quel nome a qualsiasi profondità), dopo aver seguito i link simbolici, è una cartella protetta, sta dentro una cartella così o ne contiene una. Quindi Grep su ~, /, la cartella sopra la tua home o /tmp viene negato (contengono ~/.config/aipager, ~/.claude o i file /tmp/claude-* di aipager), mentre Grep su un progetto o su /tmp/myproj no. Una regola di percorso protetto che non comincia con / o ~ (come **/.env) può corrispondere in qualsiasi cartella, quindi quando è nelle regole di un admin (o nella sezione safety:, che ricevono tutti i ruoli tranne owner) ogni ricerca dell'admin viene negata, come per i ruoli limitati; aipager doctor avvisa per una regola così nella sezione safety:. Un admin ha Bash, quindi le sue regole restano al meglio delle possibilità (più sotto).
Limiti noti. Queste cose non sono coperte:
- Se l'hook stesso fallisce prima di cominciare a decidere (per esempio non riesce a leggere il proprio file di stato, o finisce la memoria all'avvio), Claude Code lo tratta come «consenti». Un utente limitato non può provocarlo; qualsiasi errore durante la decisione nega.
- Gli strumenti che non sono strumenti sui file ma possono mandare dati fuori non vengono controllati per percorso:
WebFetch(tutto quello che l'attività può leggere lo può mandare a un URL) e gli altri strumenti di consegna di Claude Code. Negali per un ruolo inpolicy.yamlse per te conta.SendUserFile, che manda file nella chat, viene controllato (vedi file che manda Claude). - Un agente in background avviato da un'attività limitata continua a girare dopo la fine dell'attività, e le sue chiamate successive agli strumenti vengono giudicate in base al messaggio più recente della sessione: un messaggio successivo del proprietario o dal terminale toglie i suoi limiti.
- Un ruolo che definisci con
can_managema senza nessuno dei due permessi per saltare le regole non è legato a una cartella radice: può comunque partire in qualsiasi cartella di progetto in cui la sua chat lavora già (una cartella usata da una sessione precedente), compresa una delle tue, e leggere i tuoi altri progetti. - Un'attività che non si può attribuire a nessun membro (un mittente sconosciuto, un membro tolto, un messaggio da un'altra sessione il cui mittente non si può verificare) gira con la base limitata, che non ha una cartella radice. Sulle letture quella base è più debole di un
userpredefinito: un'attività così può leggere qualsiasi file della tua cartella home che non sia un percorso protetto (per esempio i tuoi altri progetti), mentre l'attività di unuserlegge solo dentro la sua cartella radice. Un'attività così può anche leggere i file caricati di tutte le chat in/tmp/aipager-files/, che l'attività di unuserlegge solo per la propria chat. Le sue scritture restano limitate alla cartella della sessione e allo scratchpad. Un messaggio verificato dalla sessione di un membro porta con sé la cartella radice di quel membro. - La cartella radice vincola solo gli strumenti sui file. Un ruolo personalizzato a cui dai
Bashpuò leggere fuori di lei tramite la shell (i predefinitiusereread_onlynon hannoBash). Un ruolo personalizzato conbypass_safetyma senzacan_managemantiene il suo permesso di saltare le regole per tutto tranne i percorsi degli strumenti sui file: nessuno strumento o schema di comandi gli viene negato. - Un motivo di rifiuto che nomina la cartella radice di un altro membro mostra il percorso di quella cartella a Claude e in chat.
- L'attività di un membro già in corso quando arrivano le cartelle radice (un aggiornamento) non ne ha nessuna fino al messaggio successivo di quel membro.
- Dentro il progetto, un utente limitato può cambiare quello che poi esegui o leggi tu (il codice, il
Makefile, gli script dipackage.json,conftest.py, unCLAUDE.mdche caricano le tue attività).
Un ruolo che ha Bash (admin, che salta le regole di divieto dei ruoli, o qualsiasi ruolo a cui policy.yaml lo restituisce: allow_tools che nomina Bash, o un deny_tools sostituito) è legato solo a regole al meglio delle possibilità: gli schemi di comandi qui sotto, che una persona determinata può aggirare. aipager doctor avvisa una volta per ogni ruolo così che un membro ha. Dai Bash solo a chi daresti una shell su questo computer.
Le regole di comando predefinite. Tra gli schemi Bash a cui è legata ogni attività da Telegram con Bash che non sia del proprietario, uno blocca la parola claude: ferma in una sola regola un claude annidato (con qualsiasi percorso, involucro o opzione) e ogni accesso a ~/.claude, e prende anche i nomi semplici dei file di controllo /tmp/claude-* di aipager, che hanno lo stesso prefisso (per quei file uno schema non è una vera protezione: una scrittura con un glob lo evita). L'unica eccezione è la cartella scratchpad che crea Claude Code, /tmp/claude-<uid>/…: proprio quella cartella, come componente intero del percorso, è esclusa, così i comandi eseguiti dallo scratchpad di una sessione non vengono fermati. È dell'utente del sistema, non di una sessione, quindi un'attività con Bash può leggere lì gli scratchpad e l'output delle attività delle altre sessioni, come può fare lo strumento Read. Un suo glob o un nome che le somiglia (/tmp/claude-1*, claude-1000x), un'altra cartella temporanea e tutto quello che si raggiunge da lì (…/../../home/you/.claude) restano bloccati. Anche gli altri usi della parola (git log --grep=claude, un file chiamato claude-notes.md, pip show claude-agent-sdk) restano bloccati, di proposito: la stessa parola nomina il programma, i suoi SDK e i file di controllo di aipager, e uno schema non li sa distinguere. Le regole che aggiungi in policy.yaml vedono il comando esattamente come è scritto, scratchpad compreso.
Ricerche tramite Bash. Alcune versioni di Claude Code non hanno lo strumento Grep o Glob (la 2.1.289 è una di queste), e allora Claude cerca con grep, rg o find tramite Bash, dove la regola di Grep/Glob qui sopra non vale. Per ogni ruolo tranne owner, gli schemi di comandi negano anche una ricerca o un elenco ricorsivo di tutta la cartella home o di /: grep -r (-R, --recursive, -d recurse, egrep, fgrep, rgrep), rg (e rg --files), ag, ack (e ack -f), find, fd, tree, du e ls -R la cui cartella è ~, ~user, $HOME, ${HOME} o "$HOME", /home, /home/<user> (o /home/$USER), /Users, /Users/<user>, /root o / (con o senza /, /. o /* finale), oppure ~/.config o ~/.local, che contengono le cartelle di aipager. Vale anche dopo cd x &&, in una pipe, dentro bash -c "...", dopo xargs, env o timeout e dopo impostazioni come LC_ALL=C. La risposta dice di cercare dentro una cartella di progetto. Le ricerche in una cartella di progetto (grep -rn x ~/proj, rg x src/, find . -name y), le ricerche dentro un progetto DI un percorso della home (grep -rn "$HOME" src: la prima parola dopo le opzioni è il testo da cercare, non una cartella) e le ricerche non ricorsive (grep x ~/.bashrc, ls ~) non vengono toccate. È al meglio delle possibilità, come ogni schema di comandi: uno schema vede le parole, non dove la shell le esegue, quindi non vede una ricerca senza cartella, o con . o .., eseguita dalla cartella home (o da sotto) (cd ~ && grep -r x .), una cartella home raggiunta con un'altra variabile o un glob, né una scritta per intero fuori da /home, /Users e /root, né \grep, né una cartella subito dopo una parola che finisce con -e o -f (presa per il testo che -e/-f danno a grep e rg, quindi ag x -f ~ passa). Non sa nemmeno quali opzioni prendono un valore a parte, quindi rg -t py "$HOME" . o grep -rn -A 3 "/home/you" src leggono py o 3 come testo e il percorso della home come cartella, e vengono negati (scrivi -A3, --include=*.py, o metti l'opzione dopo il testo), così come un percorso della home dato a -e dentro un gruppo di opzioni (grep -rne "/home" src; -r -n -e va bene); viene negato anche un comando citato dentro il testo di un altro comando (echo "a; rg x ~"), e così du -sh ~ (solo le dimensioni).
Perché un blocco ferma tutta l'attività. Dopo che una chiamata a uno strumento è stata bloccata, viene negata anche ogni chiamata successiva della stessa attività, fino al messaggio successivo. Senza questo, l'agente riproverebbe semplicemente la stessa azione scritta in un altro modo (un glob, un altro comando, un percorso codificato) finché una scrittura non passa gli schemi. Fermare l'attività rende costoso il primo tentativo; non trasforma uno schema in un confine, perché la prima scrittura provata potrebbe essere proprio quella che passa. Il costo di un falso positivo è un'attività fermata, ed è per questo che le regole qui sopra restano strette.
Vedi gruppi → come funzionano le regole per le regole che portano queste note, viste dalla parte di chi usa il bot.
File che manda Claude#
Ogni sessione avviata da aipager ha lo strumento SendUserFile di Claude Code (CLAUDE_CODE_ATTACHMENTS_BY_PATH=1), che legge file e li consegna nella chat della sessione (comandi). L'hook PreToolUse controlla ogni file della chiamata prima che Claude Code lo legga:
- La soglia di invio, per ogni attività, compresa quella dell'owner e un messaggio scritto nel terminale: la configurazione e lo stato di aipager (
~/.config/aipager/**,~/.local/share/aipager/**,~/.local/state/aipager/**e i suoi file di controllo in/tmp), tutto~/.claude/**e~/.claude.json*(le credenziali e l'account di Claude), i file di credenziali da cui è tenuto lontano ogni ruolo limitato (~/.ssh/**,~/.gnupg/**,~/.aws/**,~/.git-credentials,~/.netrce il resto di quella lista),/proc/**,/sys/**,/dev/**, e i nomi che sembrano segreti, ovunque siano (.env,.env.*,*.pem,*.key,id_rsa*,id_ed25519*e gli altri nomi di chiavi,*.p12,*.pfx,*.kdbx,*.jks,*.keystore,.pgpass,credentials,*credentials*.json). Si controlla il percorso com'è scritto e dove porta davvero, quindi anche un link simbolico verso uno di questi viene rifiutato. Alcuni file innocui hanno questi nomi (un*.keydi Keynote, un.env.example) e vengono rifiutati anche loro. - Il file stesso: un file normale una volta risolti i link simbolici, con un solo hard link, non vuoto, al massimo 50 MB.
- Le regole dell'attività: ciò che l'attività non può leggere (Read), e per un membro (un ruolo senza
can_manage, legato a una cartella radice) la regola più severa che ogni file, com'è scritto e dove porta davvero, stia nella cartella radice. A differenza delle letture di un membro, non c'è eccezione per i file fuori dalla cartella home (/tmp,/etc) né per lo scratchpad della sessione.
Un rifiuto è un rifiuto morbido: Claude legge il motivo («SendUserFile refused, nothing was sent: ... Tell the person which file could not be sent and why.») e l'attività continua; la sessione non si ferma come succede con uno strumento bloccato. Un controllo che fallisce rifiuta il file, anche per l'owner. Ogni file rifiutato ha una voce FileRefused nel log di audit.
aipager non crede sulla parola al socket di controllo per un file: qualsiasi processo locale può scriverci, nominando qualsiasi sessione, chiamata o cronologia, e basta l'hook di una chiamata a uno strumento perché una sessione ferma risulti al lavoro. Per questo una chiamata conta solo durante un'attività che aipager ha visto iniziare con un prompt (uno che ha scritto da Telegram, o un UserPromptSubmit) e durante il lavoro in background di quell'attività, finché la sessione non è ferma senza alcun lavoro in corso; a quell'inizio aipager si annota l'ora e la cronologia che segue per la sessione (un UserPromptSubmit successivo della stessa attività nomina di nuovo la cronologia). Un comando slash che aipager scrive non inizia nessuna attività (uno che esegue un'attività manda il suo UserPromptSubmit), e un'attività che non inizia mai (la sessione ancora ferma senza lavoro 15 secondi dopo) viene chiusa di nuovo. Claude Code inizia alcune attività senza UserPromptSubmit: un messaggio mandato mentre un'attività è in corso e che prende come attività successiva, e l'attività che un hook Stop fa proseguire (/goal). Un'attività così conta quando aipager ha scritto quel messaggio da sé, o appena la cronologia che si è annotato mostra il suo inizio dopo l'ultimo Stop (una riga di coda dequeue, o la riga Stop hook feedback: che scrive Claude Code); fino ad allora non si legge niente della chiamata, per al massimo 20 secondi. Prende la lista dei file dal PreToolUse che ha controllato, o dal PostToolUse quando il primo è andato perso (dopo un riavvio); mai dal risultato di Claude Code. Copia ogni chiamata una volta sola: un id di chiamata già copiato non viene più copiato né mandato (tiene gli ultimi 256 per sessione, anche dopo un riavvio). Trova una sola volta il percorso vero di ogni file e lo controlla di nuovo lì (la soglia, i controlli sul file e la cartella radice della sessione), apre proprio quel percorso senza seguire link, si assicura che il file aperto sia quello controllato e che sia ancora in quel percorso, e lo copia in ~/.local/state/aipager/outbox/<chat>/ (cartella 0700, file 0600), un posto da cui ogni attività limitata è già tenuta lontana. Prima di mandarlo, cerca la chiamata nella cronologia che si è annotato per l'attività (per la chiamata di un subagent, nella cronologia di quel subagent, accanto), mai in una che nomina il datagramma di una chiamata a uno strumento: una riga dell'assistente con un tool_use di SendUserFile con quell'id e proprio quei file, scritta dalla stessa cartella e non prima dell'inizio dell'attività, in un file .jsonl sotto ~/.claude/projects (o $CLAUDE_CONFIG_DIR/projects, come è impostato per aipager stesso) che appartiene all'utente di sistema di aipager. Claude Code scrive la sua cronologia in ritardo, quindi aipager aspetta la chiamata fino a 20 secondi, e finché il lavoro della sessione è ancora in corso (un agente in background, una chiamata a uno strumento) tiene i file e guarda di nuovo più tardi. Una chiamata che non trova ancora quando non c'è più niente in corso non parte e le sue copie vengono cancellate; una chiamata arrivata mentre non c'era un'attività del genere, o arrivata una seconda volta, non viene nemmeno copiata. Ogni loro file riceve una voce FileFailed nel log di audit con il motivo unverified. La chat riceve una riga per loro (⚠️ Non è stato possibile inviare report.pdf: aipager non è riuscito a confermare che Claude l'abbia inviato in questa attività.) solo quando la cronologia di Claude stesso mostra proprio quella chiamata durante l'attività, e la riga nomina i file che quella riga della cronologia elenca, così una chiamata finta non può mettere in chat né una riga né un nome; altrimenti in chat non arriva niente. Si manda solo la copia, solo nella chat della sessione, dopo la risposta dell'attività, dopo i file precedenti della sessione e senza trattenere l'attività dopo; viene cancellata appena mandata, o dopo un giorno.
Cosa non coprono questi controlli: un processo che gira come l'utente di sistema di aipager può scrivere le cronologie di Claude come il socket (può già leggere quei file da solo). Un processo di un altro utente che raggiunge il socket può anche fingere un UserPromptSubmit, che per questi controlli inizia un'attività e nomina la sua cronologia; poi può far mandare ad aipager, nella chat di quella sessione, un file che una vera chiamata SendUserFile ha nominato, se quella chiamata viene scritta in quella cronologia dalla stessa cartella dopo quel momento (per esempio una che un'altra sessione sta facendo proprio allora), una volta per chiamata. Per farlo deve anche mandare l'id di quella chiamata in corso (tool_use_id), che un altro utente non può leggere: si trova nelle cronologie sotto il ~/.claude dell'utente di aipager, e l'hook lo riceve dal suo input, non dalla sua riga di comando. Uno Stop finto chiude l'attività per questi controlli, così i file che quell'attività manda dopo non partono. Una sessione avviata con un $CLAUDE_CONFIG_DIR diverso da quello di aipager, o un'attività di cui aipager ha perso lo UserPromptSubmit (scritta nel terminale mentre aipager si riavviava), non manda file. Non manda file nemmeno un'attività che Claude Code inizia senza hook in un altro modo (quelli noti sono i due sopra); un file che la cronologia di Claude mostra richiesto lì riceve la riga di sopra. Il socket sta in $XDG_RUNTIME_DIR, che gli altri utenti non raggiungono, quando è impostato; altrimenti sta in /tmp.
Log di audit#
Ogni tocco su Consenti / Nega, e ogni risposta a un AskUserQuestion, aggiunge una riga JSON a ~/.claude/aipager-audit.jsonl:
{
"ts": "2026-05-18T15:42:11+00:00",
"session": "claude-jim",
"label": "jim",
"action": "Allowed",
"tool": "Bash",
"summary": "ls -la /tmp",
"user_id": 12345,
"username": "alice",
"denied": false
}
Campi (vedi aipager/audit.py per l'elenco completo):
ts: timestamp UTC in ISO 8601, al secondo.session/label: nome interno e nome leggibile.action: cosa è successo (Allowed,Denied,Allowed always, una risposta adAskUserQuestion, un rifiuto automatico, …).tool/summary: il nome dello strumento e l'input accorciato, oppure il testo della domanda.user_id/username: chi ha toccato, per sapere chi ha fatto cosa nell'uso in team;scope_label/scope_chat_idquando sono configurate più chat.denied: true per ogni rifiuto, da tocco o da regola.
Le scritture sono al meglio delle possibilità. Se il disco è pieno o ~/.claude/ non si può scrivere, aipager scrive un WARNING nel log e continua a girare: niente perdite silenziose, niente crash. Vedi aipager/audit.py.
Il log di audit sul disco riceve solo aggiunte. Insieme alla risposta di audit in chat (un messaggio di Telegram per ogni decisione, sotto la scheda di attività), ti dà due registrazioni indipendenti.
Confine dei privilegi#
aipager non aumenta mai i privilegi. Niente sudo, niente setuid, niente doas. Ogni file che scrive sta nella tua cartella home, in /tmp (i file di controllo per sessione di aipager, i download e il log del riavvio) o nella cartella che contiene il suo socket di controllo ($XDG_RUNTIME_DIR); l'elenco completo è in architettura. Ogni processo figlio (claude, dtach, gli installatori, claude update) gira come l'utente di aipager.
L'installazione della funzione vocale da Telegram ([📦 Installa la funzione vocale]) passa dall'installatore che ha installato aipager (uv tool install --reinstall aipager[voice], pipx install --force aipager[voice]) e, altrimenti, installa i pacchetti della funzione con il Python di aipager stesso (<python> -m pip install): sempre nell'ambiente di aipager, mai nel Python di sistema.
Il pulsante [🔄 Riavvia aipager], con il servizio systemd, programma un systemctl --user restart aipager.service in un servizio utente temporaneo separato; su macOS esegue launchctl kickstart -k; un aipager avviato da te crea una copia separata di sé stesso, che aspetta che il vecchio esca. Dopo un aggiornamento solo il servizio systemd si riavvia da solo (vedi comandi → aggiornamenti). Gira tutto come lo stesso utente; nessun aumento di privilegi.
Connessioni di rete#
aipager non ascolta su nessuna porta TCP fuori dal loopback. Il server della Mini App fa il bind solo su 127.0.0.1 (fissato nel codice: di proposito non c'è un'opzione host) e si raggiunge da fuori solo tramite il tunnel gestito descritto sotto. Connessioni in uscita:
- Long polling HTTPS verso
api.telegram.org(il bot di Telegram che riceve gli aggiornamenti). - HTTPS verso
telegram.orgper lo script SDK della Mini App (telegram-web-app.js), scaricato una volta all'avvio del server della Mini App e aggiornato al massimo una volta al giorno; tenuto in cache in~/.local/share/aipager/webapp-sdk/. aipager serve quei byte alla pagina dalla sua stessa origine e non li esegue mai. aipager non ridistribuisce lo script: non è incluso nel pacchetto, e quando aipager non ne ha una copia la pagina lo carica direttamente datelegram.org, come prima. - Il tunnel della Mini App verso Cloudflare, mentre è attiva (di base lo è). Il programma
cloudflaredche esegue viene scaricato una volta dalle release sugithub.com/cloudflare/cloudflared, verificato con uno SHA-256 fissato e tenuto in cache in~/.local/share/aipager/cloudflared/; uncloudflarednel tuoPATHnon viene mai usato. Niente di tutto questo succede quando usi un tuo URL per la Mini App (vedi sotto). - I controlli di versione, solo quando qualcuno tocca Cerca aggiornamenti o Aggiorna (
/update, la sezione Aggiornamenti della Mini App), o quando qualcuno che può aggiornare apre la scheda Impostazioni della Mini App (al massimo una volta ogni dieci minuti per app aperta):pypi.orgper aipager, edownloads.claude.aioregistry.npmjs.orgper Claude Code, a seconda di come è installato. Poi un aggiornamento esegue l'installatore (che parla con PyPI o Homebrew) eclaude update.aipager updatedalla riga di comando esegue solo l'installatore. - L'installazione della funzione vocale, quando tocchi il suo pulsante: l'installatore scarica i pacchetti da PyPI, e il modello di riconoscimento vocale si scarica da Hugging Face (
huggingface.co) al primo uso. - HTTPS verso
raw.githubusercontent.com(il file della chiave per le segnalazioni) e verso il server di ricezione di Sentry, solo quando confermi l'invio di una segnalazione di problemi: la segnalazione che hai visto e un breve riepilogo fatto da lei, nient'altro. Vedi Segnalazioni di problemi.
Connessioni in entrata:
- Socket Unix datagram in
$XDG_RUNTIME_DIR/aipager.sock(che torna a/tmp/aipager.sockquando$XDG_RUNTIME_DIRnon è impostato). aipager ne fa il bind e ne imposta i permessi conchmodall'avvio (aipager/dtach/hook_receiver.py). Permessi0o666, così qualsiasi processo del computer può inviargli eventi hook: lo stesso livello di fiducia di~/.claude/settings.json, che già decide cosa esegue gli hook.
Vuol dire che aipager non è una superficie di attacco remota. Un attaccante in rete non lo raggiunge senza prima avere un appiglio sul computer stesso.
Il testo di Claude e i download che fa Telegram da solo. I server di Telegram scaricano da soli alcuni indirizzi che trovano in un messaggio: un blocco di immagine, video o audio in un messaggio avanzato (rich), e la pagina dietro l'anteprima di un link. Le risposte di Claude, le sue note sulla scheda di attività, le descrizioni degli agenti e gli argomenti degli strumenti arrivano a Telegram così come Claude li ha scritti, quindi una prompt injection potrebbe far scrivere a Claude un indirizzo che porta fuori un segreto, senza nessuna chiamata a uno strumento e nessuna richiesta di permesso. aipager chiude la prima strada, e la seconda dove Telegram ha un'opzione per farlo:
- In ogni messaggio avanzato, un'immagine scritta come
viene inviata come semplice link con il suo testo alternativo, e qualsiasi altro modo di scrivere un'immagine, così come i tag<img>,<video>,<audio>,<source>,<picture>,<figure>,<iframe>,<embed>,<object>,<tg-document>,<tg-collage>,<tg-slideshow>,<tg-button>e<tg-button-row>scritti direttamente, vengono mostrati come testo (aipager/bot/media_guard.py). Dentro un blocco di codice, per esempio un pezzo di HTML, questo fa comparire un\o un<in più. - Le anteprime dei link sono disattivate in ogni messaggio di testo semplice e HTML che aipager invia o modifica (comprese le versioni in testo semplice di risposte e schede), anche quelli con il testo di aipager stesso. I link della sua Mini App sono pulsanti, che Telegram non scarica.
Telegram non documenta nessuna opzione per le anteprime nei messaggi avanzati; se Telegram ne crei una per un messaggio così non è stato verificato sul servizio reale.
Tunnel della Mini App#
Di base la Mini App è attiva, quindi su un'installazione standard aipager apre questo tunnel senza che nessuno glielo chieda. È un compromesso voluto (un'opzione da attivare che nessuno scopre è come una funzione che non c'è), ma vuol dire che una nuova installazione si collega a internet. Disattivala con aipager miniapp disable, oppure imposta enabled: false sotto miniapp: in aipager.yaml; un false scritto esplicitamente viene sempre rispettato. Nota che un blocco miniapp: mancante o rovinato ora viene letto come ATTIVO, non come DISATTIVO.
La Mini App (salvo un --url al suo posto) avvia un quick tunnel di Cloudflare gestito verso il server di loopback di aipager e pubblica l'indirizzo pubblico https://*.trycloudflare.com che riceve come pulsante del menu di Telegram e come risposta di /app.
Quell'indirizzo non è un segreto e non deve esserlo: ogni richiesta servita dalla Mini App viene controllata con la firma initData di Telegram (vedi Confine di fiducia sopra), lo stesso controllo che la protegge comunque sia stato ottenuto l'URL. Conoscere solo il nome del server non dà a un attaccante niente di più che indovinare a caso un sottodominio di trycloudflare.com: il vero cancello è il controllo della firma. Trattalo come qualsiasi URL non pubblico ma nemmeno segreto: va bene lasciarlo attivo, ma non incollarlo in un canale pubblico senza motivo.
Il nome del server sta solo in memoria: non viene mai scritto in aipager.yaml né da nessuna parte sotto ~/.config/aipager/. Un riavvio previsto (quello di un aggiornamento, Riavvia ora, il riavvio sul posto senza servizio) lascia il tunnel acceso, quindi l'indirizzo, il pulsante del menu e una Mini App aperta continuano a funzionare: il prossimo aipager lo riprende solo dopo aver controllato che sia lo stesso avvio del computer, la stessa porta, che il processo sia proprio quel cloudflared (la sua riga di comando), che dia ancora lo stesso indirizzo (~/.claude/aipager-tunnel.json ne tiene un hash, mai l'indirizzo) e che l'indirizzo risponda. In ogni altro caso lo ferma e ne avvia uno nuovo, e un tunnel tenuto che nessun aipager riprende entro 3 minuti viene fermato da un piccolo processo guardiano. Ogni altro arresto (Ctrl+C, docker stop, systemctl stop) ferma il tunnel, e un riavvio del computer o del container dà un nuovo indirizzo. MINIAPP_KEEP_TUNNEL=0 spegne questa conservazione. Se aipager viene ucciso di colpo (memoria esaurita, kill -9), il tunnel resta acceso finché aipager non riparte con il tunnel gestito attivo (la Mini App attiva, nessun MINIAPP_PUBLIC_URL) e lo riprende o lo ferma, o finché il computer non si riavvia; anche aipager uninstall lo ferma. Se il tunnel cade a metà, aipager lo riavvia con attese sempre più lunghe e ripubblica il nuovo indirizzo; se dopo alcuni tentativi non torna su, il pulsante viene tolto invece di lasciarlo puntato a un indirizzo morto.
Impostare MINIAPP_PUBLIC_URL (o aipager miniapp enable --url https://…) disattiva del tutto il tunnel gestito (il programma cloudflared non viene mai scaricato né eseguito), e l'URL dato viene usato così com'è, con lo stesso controllo initData dietro. Il rilevamento automatico di Tailscale (tailscale status --json) resta un'alternativa più semplice per chi usa già Tailscale e non vuole dipendere per niente da Cloudflare.
Trascrizione dei vocali#
faster-whisper gira dentro il processo. L'audio viene scaricato come .ogg nella cartella della chat sotto /tmp/aipager-files/ (dove arrivano i file caricati, 0700, il file 0600), trascritto in locale sulla CPU, e il file resta sotto il tuo controllo. Nessun audio lascia il computer. Nessuna API di terzi. Nessuna chiave oltre al token del bot, che serve già per parlare con Telegram.
Se cancelli il file .ogg dopo la trascrizione, l'unica traccia scritta del messaggio è la trascrizione scritta nella sessione di claude (dove segue l'informativa sulla privacy di Claude Code).
Isolamento tra sessioni#
Ogni sessione di Claude Code gira nel proprio dtach. Il socket di controllo in /tmp/claude-dtach-<name>.sock è dell'utente di aipager; dtach rifiuta le connessioni da altri utenti. Ogni sessione parte nella propria cartella, scelta con /new, con la Mini App, o la cartella in cui hai eseguito aipager session.
aipager non isola le sessioni a livello di file system: una sessione legata a ~/projects/foo in linea di principio può leggere ~/projects/bar se i permessi di claude code lo consentono. Usa un ~/.claude/settings.json per progetto o un container (l'immagine Docker) per un isolamento più stretto.
Messaggi tra sessioni di Claude#
Claude Code permette a una sessione di mandare un messaggio a un'altra (il suo strumento SendMessage). Nella sessione che lo riceve quel messaggio è un messaggio come gli altri, quindi aipager ne decide i permessi come per un messaggio di Telegram: da chi l'ha mandato davvero, mai da quello che dice.
- Il messaggio porta i permessi di chi lo manda, mai di più. Quando aipager permette a una sessione di mandare un messaggio, scrive una ricevuta che vale una volta sola: un'impronta del testo esatto, il processo di Claude che l'ha mandato (il suo numero di processo e, su Linux, la sua ora di avvio) e i permessi che aveva quell'attività. Claude Code registra il messaggio ricevuto con il numero di processo di chi l'ha mandato, verificato dal kernel, senza mai fidarsi del nome che chi manda si dà da solo. Prima che l'attività che riceve esegua qualsiasi strumento, aipager confronta le due cose e dà all'attività esattamente i permessi dell'attività che ha mandato: il messaggio del proprietario gira con i permessi del proprietario, quello di un membro con quelli del membro, un messaggio scritto nel terminale della sessione che manda senza limiti.
- Senza ricevuta, permessi limitati. Un messaggio che aipager non riesce ad abbinare (mandato da una sessione o da un processo di Claude che aipager non fa girare, una ricevuta scaduta dopo un'ora, un mittente su un altro computer) gira con le regole più restrittive più il divieto di modificare file e di accedere al web (solo leggere e pensare). I permessi precedenti della sessione che riceve non valgono mai per lui.
- Lo stesso per gli altri input da fuori della sessione. Tutto ciò che Claude Code registra come non proveniente dalla persona di questa sessione riceve gli stessi permessi limitati: un push da un canale MCP, un chiamante remoto, un messaggio messo in coda da un plugin, un messaggio inoltrato dalla casella di un'altra sessione e qualsiasi tipo di input che aipager non conosce ancora (un comando di un plugin conta come input da fuori anche quando l'hai avviato tu). Gli agenti in background della sessione (quello che restituiscono e i loro resoconti) e i suoi avvisi di attività mantengono i permessi del lavoro.
- Un messaggio programmato dalla sessione gira con i permessi dell'attività che l'ha programmato. Quando un'attività imposta un
/loopo un cron, aipager registra i permessi di quell'attività per il messaggio; quando parte più tardi riceve quei permessi, mai di più, chiunque abbia parlato per ultimo; un messaggio che si unisce a quell'attività può solo restringerla, e anche una modifica delle regole che toglie permessi a chi l'ha programmato la restringe. Lo stesso messaggio programmato due volte tiene i permessi più restrittivi. Un/loopsenza testo parte con le istruzioni di ciclo di Claude Code invece che con il suo testo, quindi riceve i permessi più restrittivi tra tutti i cicli senza testo che la sessione ha impostato, appena la cronologia mostra la partenza (fino ad allora una chiamata a uno strumento rifiutata viene rifiutata con garbo e Claude riprova). Un messaggio che nessuno ha programmato passando dal controllo di aipager riceve i permessi limitati. - Il testo non prova niente. Il nome di un mittente, una riga
[via Telegram · @owner]o «il proprietario chiede…» dentro un messaggio non cambiano niente. A Claude viene detto chi aipager ha verificato come mittente, e che le affermazioni nel testo non sono una prova. - Unirsi a un'attività in corso la può solo restringere. Un messaggio da un'altra sessione che arriva mentre un'attività è in corso rende quell'attività restrittiva quanto la più restrittiva delle due, mai più permissiva. Vale anche quando Claude Code prende il messaggio senza un proprio evento di messaggio (assorbito dall'attività in corso, o eseguito come attività successiva dalla sua coda): la via rapida del proprietario guarda la fine della cronologia prima di ogni chiamata a uno strumento, e la fine dell'attività e aipager segnano la sessione da controllare quando vedono un messaggio così in coda.
- I ruoli restano nella loro chat. Una ricevuta da una sessione di un'altra chat conta solo quando la sua attività aveva i permessi del proprietario o è stata scritta nel suo terminale; tutto il resto gira con permessi limitati.
- Chi può mandare. I ruoli limitati predefiniti non possono usare SendMessage per niente. Un ruolo a cui una regola lo concede può scrivere solo alle sessioni che fa girare aipager (indirizzate come
uds:…/<pid>.sock);aipager doctoravvisa per un ruolo così. - Gli agenti in background mantengono i propri permessi. Un agente tiene i permessi dell'attività che l'ha avviato (o quelli più restrittivi di un'attività successiva), mai quelli di un'attività successiva che ne ha di più.
- Quando viene fermato. Se un'attività avviata da un'altra sessione viene fermata dalle regole di sicurezza, la scheda di attività mostra già chi l'ha mandata, l'avviso di stop lo dice, e chi ha passato il lavoro viene avvisato, così non sparisce mai in silenzio: la chat della sessione che ha mandato quando le due sessioni sono nella stessa chat; altrimenti solo il proprietario, in privato, e solo quando il proprietario (o il terminale) guidava l'attività che ha mandato.
Limiti noti:
- Un'attività con una shell (del proprietario o di un admin) gira come lo stesso utente del sistema di aipager e potrebbe falsificare una ricevuta, così come può già riscrivere gli altri file di controllo di aipager. È il modello di fiducia di sempre: dai una shell solo a chi affideresti il computer.
- Le sessioni di Claude che aipager non fa girare non sono gestite da aipager per niente, chiunque scriva loro.
- Fuori da Linux, chi manda viene abbinato solo con il numero di processo (lì Claude Code non registra l'ora di avvio). Su Linux una registrazione ricevuta senza ora di avvio non corrisponde a niente.
- Claude Code può scrivere il messaggio nella cronologia un attimo dopo la prima chiamata a uno strumento dell'attività. Finché non lo fa, una chiamata che i permessi limitati rifiuterebbero viene rifiutata con garbo (a Claude viene detto di riprovare; l'attività non si ferma). Dopo 8 chiamate senza, l'attività gira con permessi limitati.
- La via rapida del proprietario legge l'ultimo 1 MiB della cronologia, mai di più. Un messaggio da un'altra sessione più lungo di così finisce con una riga che la via rapida prende per un messaggio del genere (qualsiasi riga di oltre 256 KiB tagliata dalla finestra lo è). Uno che sta tutto più indietro (più di 1 MiB di cronologia scritto dopo di lui prima della chiamata successiva) lo prende aipager mentre l'attività gira, o la fine dell'attività mentre è ancora in coda, non la via rapida.
- Mentre una sessione ha un
/loopsenza testo impostato, qualsiasi attività non avviata da un messaggio di Telegram (compreso un messaggio scritto nel terminale) può vedersi rifiutare con garbo le prime chiamate agli strumenti, finché Claude Code non ha scritto il messaggio di quell'attività e aipager può capire che non è il ciclo che parte; poi Claude riprova. - aipager legge l'attività di un messaggio programmato da quello che la cronologia ha guadagnato da quando è cominciata; oltre gli 8 MiB, dai primi e dagli ultimi 4 MiB (quello che sta in mezzo è stato controllato quando era la parte più recente).
- I permessi registrati per un messaggio programmato restano finché la sessione non viene terminata (
/kill): una sessione terminata o ripresa li tiene, come Claude Code tiene i suoi cron e un risveglio di/loopin attesa. Se ne tengono al massimo 256 per sessione, e programmarne un altro oltre quel numero viene rifiutato a tutti nella sessione, quindi chiunque possa programmare può esaurirli (allora avvia una sessione con un altro nome, o fai/killdi questa). Un messaggio che parte senza registrazione riceve i permessi limitati, o in una sessione con un/loopsenza testo i permessi di quel ciclo, appena Claude Code l'ha scritto nella cronologia; fino ad allora aipager legge l'attività da quella precedente. Succede per un messaggio impostato prima di questa versione; dopo un riavvio del computer (le registrazioni stanno nella cartella temporanea del sistema, che un riavvio svuota, mentre Claude Code può ancora riportare il risveglio di una sessione ripresa entro l'ora); e per un cron durevole, che Claude Code esegue in un'altra sessione della stessa cartella quando la sua sessione è terminata. Una sessione con meno permessi che programma lo stesso testo può abbassare i permessi di un messaggio (mai alzarli). - Una sessione avviata da zero con il nome di una sessione precedente (Sostituisci) tiene le registrazioni di quella sessione, compresi i permessi del suo
/loopsenza testo, fino a/kill. - Un messaggio programmato che Claude Code esegue subito dopo un'altra attività, senza un proprio evento di messaggio (dalla sua coda), riceve i più restrittivi tra i propri permessi e quelli dell'attività che segue (dopo un'attività scritta nel terminale, esattamente i propri).
- Le prime chiamate agli strumenti di un'attività scritta nel terminale possono essere rifiutate finché Claude Code non ha scritto il suo messaggio nella cronologia (fino ad allora aipager legge l'attività da quella precedente).
- Un agente in background avviato con permessi più stretti può restituire testo a un'attività successiva dello stesso lavoro, che lo legge con i permessi del lavoro, come per gli altri avvisi di attività di Claude Code.
- Claude Code può trattenere un messaggio in attesa di approvazione nel terminale che lo riceve (la sua impostazione
crossSessionInbound, o quando le due sessioni girano con classi di permessi diverse, per esempio «Completo» e «Automatico»). aipager non può sbloccare un messaggio così da Telegram; la ricevuta aspetta fino a un'ora.
Riepilogo delle minacce#
| Minaccia | Contromisura |
|---|---|
| Uno sconosciuto manda comandi al bot | Il filtro sull'ID della chat li rifiuta |
| Furto del token del bot | /revoke in @BotFather, nuova configurazione |
| Una chiamata a uno strumento di claude viene sfruttata | Il controllo di sicurezza di aipager nega quello che le regole di un'attività da Telegram vietano; in modalità «Chiedi» le regole di Claude Code in settings.json decidono cosa chiedere e il tocco di una persona è l'unico modo per consentire; in modalità «Automatico» (la predefinita per ogni nuova sessione) il controllo di sicurezza di Claude Code esegue senza chiedere le chiamate che considera sicure e ferma quelle rischiose perché decida una persona; una sessione in modalità «Completo» (admin, e una persona nella propria chat privata) non chiede niente, quindi resta solo il controllo di sicurezza di aipager, e per le attività del proprietario stesso e i messaggi scritti nel terminale non resta niente (chi decide) |
| Un utente limitato di Telegram arriva ai permessi del proprietario | Niente shell, sessioni avviate solo dentro la cartella radice del membro, scritture e ricerche limitate al progetto lì dentro (mai la cartella home), niente letture nella cartella home fuori dalla cartella radice, cartelle radice degli altri membri e file caricati delle altre chat irraggiungibili, file con le credenziali illeggibili, percorsi protetti per gli strumenti sui file, un controllo che si rompe nega (applicazione delle regole nell'uso in team); al meglio delle possibilità per un ruolo a cui è stato dato Bash, e senza i casi in «Limiti noti» |
| Il messaggio di una sessione sostiene di avere i permessi del proprietario | Il messaggio gira con i permessi provati dell'attività che l'ha mandato, mai con quelli precedenti della sessione che lo riceve; senza prova, solo leggere e pensare (messaggi tra sessioni) |
| Un membro del gruppo prende in prestito i permessi di un altro | Ogni messaggio gira con il ruolo del proprio mittente nella chat da cui arriva; il messaggio di un altro mittente aspetta l'attività in corso; se si unisce comunque, vince la regola più restrittiva; i comandi slash dei ruoli limitati si fermano ai comandi della tastiera (applicazione delle regole nell'uso in team) |
| Le chiacchiere del gruppo arrivano in una sessione | In un gruppo vengono elaborati solo comandi, risposte al bot, menzioni e tocchi segnati sulla tastiera (gruppi) |
| Un membro tolto mantiene l'accesso | aipager config ricarica aipager senza riavvio; i suoi messaggi trattenuti vengono scartati e una sua attività ancora in corso perde i vecchi permessi; toglierlo dal gruppo di Telegram gli impedisce di leggere il gruppo (gruppi → ricaricare senza riavviare) |
| Manomissione del log di audit | Solo aggiunte; impedirla del tutto è fuori portata senza un programma di firma separato |
| Una prompt injection fa scrivere a Claude un indirizzo che porta fuori dei dati | Le immagini e i tag multimediali nel testo di Claude vengono mostrati come link o come testo, mai come contenuti multimediali, e le anteprime dei link sono disattivate nei messaggi di testo semplice e HTML (connessioni di rete) |
| Attaccante in rete | Nessuna porta in entrata, nessun accesso diretto |
| Aumento dei privilegi sul computer | Niente sudo / setuid; aipager resta nello spazio utente |
| L'audio dei vocali finisce nel cloud | La trascrizione avviene sul computer |
| L'agente legge la credenziale di Claude di aipager | Previsto, non un bug: vedi La credenziale di Claude. Limita la credenziale stessa, non il file che la contiene |
Vedi anche#
- Architettura: come è organizzato il processo.
- Eventi hook: cosa vede davvero aipager da claude.
- Comandi del bot: il lato guidato dalle persone.