SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor

Claude Code su VPS con tmux: sessioni SSH persistenti

Esegui Claude Code su un VPS Linux sempre acceso con tmux: le sessioni sopravvivono alla disconnessione SSH. Installazione, sicurezza ed errori da evitare.

Il problema è il coperchio del laptop, non la CLI

Claude Code funziona correttamente sul laptop finché non lo chiudi: la sessione SSH termina, la shell riceve un segnale SIGHUP e l’agent si interrompe dopo tre minuti di esecuzione del test. Esegui la CLI su una macchina che non entra mai in sospensione, all’interno di un multiplexer di terminale i cui processi non siano figli della sessione SSH. Questo è tutto il meccanismo. Il componente essenziale è tmux, non l’installazione.

Questa pagina riguarda la gestione di una macchina su cui lasciare gli agent in esecuzione. Se non hai un server Linux che puoi lasciare acceso, queste istruzioni non si applicano. Questo è l’unico prerequisito da dichiarare con chiarezza.

Che cosa fa realmente tmux

Quando stabilisci una connessione SSH, sshd crea un processo figlio con una shell e gli assegna un terminale pseudoterminale; tutto ciò che avvii da quella shell è un suo processo figlio. Se la connessione cade, il kernel elimina il pty, la shell riceve SIGHUP e a sua volta chiude la connessione con i processi figli. I processi in esecuzione in primo piano e di lunga durata terminano.

tmux inverte la gerarchia dei processi. Il comando tmux che digiti è un client leggero che comunica tramite un socket Unix con un server tmux, eseguito separatamente dal terminale. Le shell all'interno di una sessione sono processi figli del server, non di sshd. Se termini la connessione SSH, il client scompare, mentre il server, la sessione e l'agente impegnato nell'attività continuano a funzionare. Riconnettiti, esegui tmux attach e tornerai alla stessa shell con lo stesso scrollback. Anche nohup sopravvive a un hangup, ma non offre alcun modo per rientrare nella sessione: non puoi ricollegarti a una TUI eseguita in background. Claude Code è interattivo; tmux (o screen) è lo strumento appropriato.

Dimensionamento del server

La CLI è un singolo binario nativo; non è ciò che riempie il server. A occupare spazio e risorse è ciò che l’agente esegue per conto tuo: una build, un’intera suite di test, tsc, un language server o un database in Docker. Dimensiona il server in base alla toolchain, non alla CLI. Aggiungi anche lo swap, persino se prevedi di non usarlo mai: trasforma un arresto forzato per OOM in una build lenta:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Controlla anche lo spazio su disco: repository, node_modules e immagini Docker si accumulano rapidamente. Se la toolchain usa, oltre ai container, macchine virtuali complete, ad esempio un guest KVM o un nodo Kubernetes locale, verifica prima di procedere che il piano esponga le estensioni di virtualizzazione della CPU. Eseguire la virtualizzazione annidata su un VPS è infatti un’operazione che il provider abilita per te, non un’impostazione che puoi attivare dall’interno del guest.

Prima un utente non-root

Creare un utente dedicato con la propria home e installare la chiave pubblica:

sudo adduser --disabled-password --gecos "" agent
sudo install -d -m 700 -o agent -g agent /home/agent/.ssh
sudo cp ~/.ssh/authorized_keys /home/agent/.ssh/authorized_keys
sudo chown agent:agent /home/agent/.ssh/authorized_keys
sudo chmod 600 /home/agent/.ssh/authorized_keys

Prima di procedere, verificare l'accesso da un secondo terminale, mentre l'autenticazione tramite password è ancora disponibile come fallback. Se viene restituito Accesso negato (publickey), il problema è solitamente il proprietario o la modalità della directory .ssh, non la chiave.

Per scelta, agent non appartiene al gruppo sudo. Se serve un pacchetto di sistema, lo si installa. Questa decisione elimina la maggior parte dei casi in cui un comando di shell eseguito per errore può danneggiare l'host.

Igiene SSH per un server lasciato sempre in esecuzione

L’autenticazione tramite password su una macchina esposta tutto il giorno a Internet, che contiene un agent e il codice sorgente, comporta un rischio non necessario. Disattivala. Su Ubuntu 24.04 e Debian 13, /etc/ssh/sshd_config include /etc/ssh/sshd_config.d/*.conf, quindi crea un file dedicato invece di modificare la configurazione principale:

# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

Evita di inserire il key exchange in quel file, salvo che ci sia un motivo specifico: una riga KexAlgorithms copiata da una vecchia guida di hardening spesso impone esclusivamente algoritmi classici. Per questo un client SSH recente può visualizzare l'avviso che la connessione non usa un key exchange post-quantistico.

Verifica la configurazione e ricaricala. Mantieni aperta la sessione corrente mentre testi una nuova sessione da un secondo terminale:

sudo sshd -t && sudo systemctl restart ssh

Su Ubuntu 24.04 c’è una particolarità: sshd viene attivato tramite socket. Le impostazioni di autenticazione si applicano su systemctl restart ssh, ma una modifica a Port richiede anche systemctl daemon-reload e il riavvio di ssh.socket.

Poi configura il firewall. Consenti SSH prima di abilitarlo, altrimenti perdi l’accesso:

sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

Installa fail2ban sapendo esattamente cosa offre: dopo aver disattivato l’autenticazione tramite password, un attacco brute force non può comunque avere successo; il pacchetto impedisce però che i tentativi falliti finiscano nel journal.

# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1h

Infine, applica automaticamente le patch con sudo apt install unattended-upgrades e sudo dpkg-reconfigure -plow unattended-upgrades. Considera l’interazione con tmux: abilita Unattended-Upgrade::Automatic-Reboot e un aggiornamento del kernel riavvierà il server, terminando tutte le sessioni. Lascialo disabilitato e riavvia secondo il tuo programma, quando non ci sono attività in esecuzione. La stessa cautela vale per un aggiornamento di release: passare il server da Ubuntu 24.04 a 26.04 riavvia sshd e il kernel, quindi va pianificato in una finestra in cui nessuna sessione tmux stia eseguendo attività importanti.

Installare Node.js e Claude Code su Ubuntu

Il metodo basato su npm richiede Node.js 22 o versioni successive. (Il pacchetto ora include un binario nativo che non viene eseguito da Node, ma npm controlla comunque la versione del motore e la toolchain dell'agente richiede in genere una versione aggiornata di Node.) Il pacchetto della distribuzione è spesso obsoleto; su Ubuntu e Debian si usa normalmente NodeSource, che fornisce un repository firmato (senza apt-key, deprecato su Ubuntu 24.04 e già rimosso da Debian 13):

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --version

Ecco il punto in cui si commette spesso un errore: installate la CLI come utente agent, mai con sudo npm -g. Un prefisso globale di proprietà di root causa in seguito errori di autorizzazione e lascia file di proprietà di root nella cache di npm. Impostate prima il prefisso di npm sulla home dell'utente:

mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g @anthropic-ai/claude-code
claude --version

L'export va inserito in ~/.bashrc, sopra il controllo "If not running interactively, don't do anything" vicino all'inizio del file. Un pannello tmux predefinito esegue una shell di login: tmux lascia vuoto default-command, indicando di avviare default-shell come shell di login, e una bash di login legge ~/.profile, che su Ubuntu esegue il source di ~/.bashrc. Le shell che non eseguono il login, ad esempio quando impostate personalmente default-command, leggono direttamente ~/.bashrc. In questo modo ~/.bashrc copre entrambi i casi, mentre solo ~/.profile non coprirebbe il secondo. C'è un'eccezione: bash ignora ~/.profile se esiste ~/.bash_profile o ~/.bash_login; quindi, se ne avete creato uno, fate in modo che esegua anch'esso il source di ~/.bashrc. Un'installazione di Node per utente tramite un gestore di versioni come nvm ottiene lo stesso risultato; in entrambi i casi, l'obiettivo è fare in modo che npm install -g non debba mai usare sudo. npm continua a funzionare correttamente, ma la documentazione di Anthropic indica lo script di installazione nativo come metodo consigliato; sono inoltre disponibili repository apt, dnf e apk firmati. Consultate la documentazione di installazione di Anthropic prima di incollare i comandi, perché i metodi di installazione cambiano.

Eseguite claude all'interno di un repository per avviarlo. La prima esecuzione guida l'autenticazione; un server senza interfaccia grafica non dispone di un browser, quindi il flusso fornisce un URL da aprire sul proprio computer e un codice da riportare nel terminale. (L'altra possibilità consiste nell'impostare una API key nell'ambiente.) In entrambi i casi, quella credenziale viene ora memorizzata sul server. Questo ci porta alla parte che spesso viene ignorata.

Il problema dell'ampiezza dell'impatto

Un agente con accesso alla shell è una shell. Può leggere tutto ciò che l'utente con cui viene eseguito può leggere e inviare modifiche ovunque quell'utente abbia i permessi per farlo. Non è una critica allo strumento: è la sua definizione. Per questo l'account con cui viene eseguito è più importante di qualsiasi singola impostazione.

  • Utente dedicato senza privilegi. Nessun gruppo sudo e nessuna directory home condivisa con il proprio account.
  • Nessuna credenziale di produzione sul server. Nessun ~/.aws/credentials contenente chiavi di produzione, nessun .env copiato dall'ambiente di produzione e nessuna password del database con accesso in scrittura a risorse importanti. Fornire all'agente una credenziale di staging o in sola lettura.
  • Token con ambito limitato. Un token GitHub con autorizzazioni granulari limitato a un solo repository; una deploy key quando è sufficiente l'accesso in lettura.

Claude Code include il flag --dangerously-skip-permissions, che salta quasi tutte le richieste di autorizzazione. Su un portatile o in un progetto usa-e-getta, la scelta spetta a voi. Su un server che contiene token, elimina l'ultima barriera tra un'istruzione interpretata erroneamente e un git push --force. Inoltre, le richieste che verrebbero saltate non sono una scelta binaria. Con la modalità automatica ora impostata come modalità iniziale predefinita (a partire da Claude Code v2.1.283), è utile sapere quale modalità di autorizzazione assegnare in modo vincolante a un server che non state monitorando. La sezione eseguire Claude Code in sicurezza su un server spiega cosa cambia effettivamente il flag e come contenere un agente che lo usa, dalla sandbox integrata fino a un VPS usa-e-getta.

Deploy key e inoltro dell'agente SSH

Può essere allettante ssh -A in modo che git possa usare la chiave presente sul portatile. È importante capire cosa comporta: l'inoltro dell'agente espone il socket dell'agente SSH locale ai processi eseguiti con quell'utente sul server. Qualsiasi processo eseguito come agent, incluso l'agente, può chiedere alla chiave di firmare per qualsiasi host raggiungibile, finché la connessione resta attiva. È molto più di un semplice «consentire a git di eseguire il pull di questo repository».

Generate invece una chiave sul server, registratela come deploy key specifica per il repository (con accesso in scrittura solo se l'agente deve eseguire il push) e configurate un'identità git, in modo che i commit creati dal server siano riconoscibili:

ssh-keygen -t ed25519 -C "agent deploy key" -f ~/.ssh/id_ed25519_repo
cat ~/.ssh/id_ed25519_repo.pub   # paste into the repo's Deploy Keys
git config --global user.name "Agent (build box)"
git config --global user.email "agent@example.com"

Il flusso di lavoro con tmux

Installalo (sudo apt install tmux), quindi crea una configurazione minima ~/.tmux.conf:

set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"

Quattro comandi coprono l'uso quotidiano:

tmux new -A -s claude     # attach to session "claude", creating it if absent
# ...run `claude` inside it, work normally...
# Ctrl-b then d           -> detach; everything keeps running
tmux ls                   # list sessions
tmux attach -t claude     # reattach, from this machine or any other
tmux kill-session -t claude

tmux new -A -s claude è il comando da memorizzare: si collega alla sessione se esiste e la crea se non esiste. Un solo comando copre quindi sia l'avvio della giornata sia la ripresa del lavoro dopo una disconnessione. Creane un alias. All'interno di una sessione, Ctrl-b c apre una finestra, mentre Ctrl-b n e Ctrl-b p permettono di scorrerle. Ctrl-b [ attiva la modalità di copia per scorrere indietro (q la chiude).

È importante sapere una cosa sulle sessioni che non vengono mai terminate: l'agente invia nuovamente l'intera conversazione a ogni turno. Prima di lasciare una sessione attiva per una settimana, leggi come una sessione persistente di Claude Code consuma i token.

Modalità di errore

"La mia sessione è scomparsa." tmux ls stampa no server running on /tmp/tmux-1000/default. Quasi sempre significa che il processo non era mai stato avviato dentro tmux: hai effettuato l'accesso tramite SSH, eseguito direttamente claude e la disconnessione ha terminato il processo. Non c'è nulla da recuperare. L'abitudine che evita il problema è questa: tmux new -A -s <project> deve essere il primo comando dopo ogni accesso.

Il riquadro si riduce a una casella minuscola. L'opzione window-size stabilisce quale client collegato determina le dimensioni. A partire da tmux 3.1 (Ubuntu 24.04 include la versione 3.4), il valore predefinito è latest, cioè il client con l'attività più recente. Tuttavia, una configurazione che contiene window-size smallest usa le dimensioni del client collegato più piccolo; di conseguenza, un client inattivo ancora collegato da un'altra macchina restringe la visualizzazione. Forza la disconnessione degli altri client durante il collegamento: tmux attach -d -t claude.

Una compilazione stampa Killed. Una sola riga, senza stack trace. Verifica con sudo dmesg -T | grep -i -E 'out of memory|killed process': il kernel ha terminato il processo che consumava più memoria tramite l'OOM killer. Da Node potresti invece vedere FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Procedi nel seguente ordine: aggiungi lo swap (come indicato sopra), limita il parallelismo dei test e del compilatore, aumenta l'heap di Node con NODE_OPTIONS=--max-old-space-size=... oppure passa a un VPS con più risorse. L'OOM killer può terminare anche il server tmux invece della compilazione, facendo scomparire la sessione. Se è in esecuzione systemd-oomd, può terminare un'intera slice dell'utente con lo stesso effetto.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Un'installazione globale in un prefix di proprietà di root. Usa il prefix ~/.npm-global indicato sopra. Se in precedenza hai eseguito sudo npm, potresti vedere anche Your cache folder contains root-owned files; correggi il problema con sudo chown -R $(id -u):$(id -g) ~/.npm.

claude: command not found, ma solo a volte. L'export PATH si trova in ~/.bashrc sotto il controllo "If not running interactively, don't do anything", quindi le shell non interattive lo ignorano. Sposta l'export sopra quel controllo e mantienilo in ~/.bashrc: un riquadro tmux predefinito è una shell di login che raggiunge ~/.bashrc tramite ~/.profile, mentre qualsiasi shell non di login legge direttamente ~/.bashrc. In questo modo, un solo file copre entrambi i casi.

Colori illeggibili dopo il collegamento. Si tratta di una mancata corrispondenza di TERM; la riga default-terminal riportata sopra risolve il problema.

Le sessioni scompaiono dopo un riavvio. Non è un bug: il server tmux è un processo e il riavvio lo termina. Verifica uptime.

Cosa si rompe quando il sistema cresce

Più progetti. Usa una sessione tmux per ogni repository e assegnale il nome del repository; tmux ls diventa così il tuo pannello di controllo. Se non mantieni questa convenzione, ottieni sessioni 0, 1, 2. Quando ne esegui diverse contemporaneamente, non devono necessariamente operare in isolamento: una sessione può inviare un messaggio a un'altra sullo stesso host, utile quando l'agent che sta eseguendo un refactoring lungo vuole incaricare un secondo agent di eseguire i test. Anche le porte si moltiplicano nello stesso modo: quando sei arrivato a sei repository che richiedono tutti :3000, devi smettere di assegnarle manualmente e lasciare che un reverse proxy Traefik esegua il routing di più applicazioni tramite Docker Compose usando il nome host.

Più persone. I socket tmux sono specifici dell'utente, quindi due sviluppatori sullo stesso host hanno ciascuno il proprio server tmux e non possono vedere le sessioni dell'altro. Condividere una sessione tramite un socket condiviso significa che tutti digitano nella stessa shell con lo stesso utente Unix, con le conseguenze che ne derivano per auditing e autorizzazioni. Usare utenti separati è la scelta semplice e corretta.

Lavoro non presidiato. tmux è progettato per sessioni interattive a cui ci si collega. I job pianificati che devono essere eseguiti senza supervisione appartengono a una unit e a un timer systemd, che forniscono logging, una policy di riavvio e la continuità dopo il boot. Usare tmux per eseguire un job di tipo cron indica che il job dovrebbe essere un servizio.

Un'ultima nota: associa i dev server avviati dall'agent a 127.0.0.1, non a 0.0.0.0, e raggiungili tramite un tunnel SSH (ssh -L 3000:127.0.0.1:3000 agent@your-server) invece di aprire porte in ufw. Quando inoltri una mezza dozzina di porte, oppure un telefono e un laptop devono accedere entrambi allo stesso ambiente di anteprima, configura invece una VPN WireGuard self-hosted sul VPS davanti a questi servizi: i dev server restano in ascolto su un'interfaccia privata e ufw continua a negare tutte le connessioni provenienti da quella pubblica. Il firewall è utile solo se smetti di aprirvi continuamente nuove eccezioni.

Claude Code non è l'unica scelta: eseguire un agent AI per la programmazione su un VPS significa valutare anche Aider e Goose.

FAQ

Claude Code continua a essere eseguito dopo l'interruzione della connessione SSH?

Solo se lo hai avviato all'interno di tmux. Un processo avviato direttamente dalla shell SSH è figlio di quella shell e termina con il pty quando la connessione cade. All'interno di tmux, la shell appartiene al server tmux scollegato, quindi l'agent continua a lavorare sull'attività in corso e tmux attach ti riporta alla stessa cronologia del terminale. Imposta tmux new -A -s <project> come primo comando dopo ogni accesso e il problema scompare.

Devo installare la CLI con sudo npm install -g?

No. Un prefisso globale di proprietà di root provoca errori EACCES nelle installazioni successive e crea file di proprietà di root nella cache di npm. Imposta il prefisso di npm su ~/.npm-global (oppure usa un version manager come nvm), installa come utente non privilegiato agent ed esporta ~/.npm-global/bin in PATH da ~/.bashrc, prima del controllo della shell interattiva. Se hai già eseguito sudo npm una volta, ripara la cache con sudo chown -R $(id -u):$(id -g) ~/.npm.

Il forwarding dell'agent ssh -A è sicuro su un server che esegue un agent?

Concede molti più privilegi di quelli necessari all'attività. Il forwarding espone il socket dell'agent SSH locale a ogni processo eseguito da quell'utente, quindi qualsiasi processo sul server può chiedere alla tua chiave di firmare per qualunque host raggiungibile, per tutto il tempo in cui resti connesso. Genera una chiave ed25519 sul server e registrala come deploy key per il singolo repository, concedendo l'accesso in scrittura solo se l'agent deve effettivamente eseguire operazioni di push.

Perché la mia build stampa soltanto Killed?

Una sola parola senza stack trace indica l'intervento dell'OOM killer del kernel. Confermalo con sudo dmesg -T | grep -i -E 'out of memory|killed process'; da Node potresti vedere invece JavaScript heap out of memory. Applica le correzioni nell'ordine: aggiungi un file di swap, limita il parallelismo dei test e del compilatore, aumenta NODE_OPTIONS=--max-old-space-size=..., quindi passa a un VPS più grande. Tieni presente che l'OOM killer può terminare il server tmux invece della build, interrompendo l'intera sessione.

tmux o un servizio systemd?

tmux è adatto alle sessioni interattive a cui ti connetti, che osservi e in cui immetti comandi. È esattamente il caso di una sessione dell'agent. Le attività pianificate senza un operatore appartengono a un'unità e a un timer systemd, che offrono logging, una policy di riavvio e la continuità dopo il boot senza configurazioni aggiuntive. Se stai usando tmux per eseguire un'attività simile a un cron job, quell'attività dovrebbe essere un servizio.