Claude Code sur un VPS distant avec tmux
Exécutez Claude Code sur un VPS Linux toujours actif dans tmux pour que les sessions survivent aux coupures SSH. Installation, durcissement et pannes à prévoir.
Le problème, c'est le capot du portable, pas la CLI
Claude Code fonctionne très bien sur votre portable jusqu'à ce que vous le fermiez : la session SSH meurt, le shell reçoit un SIGHUP, et l'agent, trois minutes après le début d'une série de tests, meurt avec lui. Exécutez la CLI sur une machine qui ne dort jamais, dans un multiplexeur de terminal dont les processus ne sont pas des enfants de votre session SSH. C'est là toute l'astuce, et c'est tmux, pas l'installation, qui porte tout le reste.
Cette page parle de l'exploitation d'une machine sur laquelle vous laissez des agents tourner. Si vous n'avez aucun serveur Linux que vous pouvez laisser allumé, rien de tout cela ne s'applique. C'est le seul vrai prérequis.
Ce que fait réellement tmux
Quand vous vous connectez en SSH, sshd crée un shell par fork et lui attribue un pseudo-terminal ; tout ce que vous lancez depuis ce shell en est un enfant. Coupez la connexion et le noyau détruit le pty, le shell reçoit un SIGHUP, et il raccroche à son tour au nez de ses enfants. Les processus de premier plan de longue durée meurent.
tmux inverse la propriété. La commande tmux que vous tapez est un client léger qui dialogue via un socket unix avec un serveur tmux qui tourne détaché de votre terminal. Les shells d'une session sont des enfants de ce serveur, pas de sshd. Coupez la connexion SSH et le client disparaît tandis que le serveur, la session et l'agent en pleine tâche continuent de tourner. Reconnectez-vous, tmux attach, et vous revoilà dans le même shell avec le même historique d'affichage. nohup survit lui aussi à un raccrochage, mais ne vous donne aucun moyen de revenir : vous ne pouvez pas vous rattacher à une interface texte passée en arrière-plan. Claude Code est interactif ; tmux (ou screen) est le bon outil.
Dimensionner la machine
La CLI est un processus Node ; ce n'est pas elle qui remplit la machine. Ce qui remplit la machine, c'est tout ce que l'agent exécute pour vous : une compilation, une suite de tests complète, tsc, un serveur de langage, une base de données dans Docker. Dimensionnez pour la chaîne d'outils, pas pour la CLI. Ajoutez du swap même si vous comptez ne jamais y toucher : cela transforme un arrêt OOM brutal en une compilation lente :
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/fstabSurveillez aussi le disque : les dépôts, node_modules et les images Docker s'accumulent vite. Et si la chaîne d'outils va au-delà des conteneurs jusqu'à des machines virtuelles complètes, un invité KVM, un nœud Kubernetes local, vérifiez que l'offre expose les extensions de virtualisation du CPU avant de vous engager, car faire tourner de la virtualisation imbriquée sur un VPS est quelque chose que le fournisseur active pour vous plutôt que quelque chose que vous activez depuis l'intérieur de l'invité.
D'abord un utilisateur non-root
Créez un utilisateur dédié avec son propre répertoire personnel, et mettez votre clé publique en place :
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_keysDélibérément, agent n'est pas dans le groupe sudo. Si un paquet système est nécessaire, c'est vous qui l'installez. Cette seule décision élimine la plupart des façons dont une commande shell égarée peut détruire l'hôte.
Hygiène SSH pour une machine que vous laissez tourner
L'authentification par mot de passe sur une machine exposée à l'internet public toute la journée, contenant un agent et votre code source, n'est pas un risque qui vaut la peine d'être pris. Désactivez-la. Sur Ubuntu 24.04 et Debian 13, /etc/ssh/sshd_config inclut /etc/ssh/sshd_config.d/*.conf, alors déposez-y un fichier plutôt que de modifier la configuration principale :
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin noValidez et rechargez, en gardant votre session actuelle ouverte pendant que vous en testez une nouvelle depuis un second terminal :
sudo sshd -t && sudo systemctl restart sshUne nuance sur Ubuntu 24.04 : sshd est activé par socket. Les paramètres d'authentification s'appliquent avec systemctl restart ssh, mais un changement du Port d'écoute nécessite aussi systemctl daemon-reload et un redémarrage de ssh.socket.
Ensuite le pare-feu. Autorisez SSH avant de l'activer, sinon vous vous verrouillez dehors :
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enableInstallez fail2ban en gardant l'esprit clair sur ce qu'il vous apporte : une fois l'authentification par mot de passe désactivée, la force brute ne peut de toute façon pas réussir, il garde simplement les tentatives échouées hors de votre journal.
# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1hEnfin, appliquez les correctifs automatiquement avec sudo apt install unattended-upgrades et sudo dpkg-reconfigure -plow unattended-upgrades. Notez l'interaction avec tmux : activez Unattended-Upgrade::Automatic-Reboot et une mise à jour du noyau redémarre la machine, emportant toutes les sessions avec elle. Laissez-le désactivé et redémarrez selon votre propre calendrier, quand rien n'est en cours d'exécution.
Installer Node.js et Claude Code sur Ubuntu
Claude Code est une CLI Node, il vous faut donc une version récente de Node. Le paquet de la distribution est souvent en retard ; NodeSource est la voie habituelle sur Ubuntu et Debian, et il fournit un dépôt signé (pas d'apt-key, cet outil a disparu) :
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --versionMaintenant la partie que les gens ratent : installez la CLI en tant qu'utilisateur agent, jamais avec sudo npm -g. Un préfixe global appartenant à root produit des erreurs de permission plus tard et laisse des fichiers appartenant à root dans le cache npm. Pointez d'abord le préfixe de npm vers le répertoire personnel de l'utilisateur :
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 --versionL'export va dans ~/.bashrc, pas dans ~/.profile, et il doit se trouver au-dessus de la garde « If not running interactively, don't do anything » près du haut du fichier : tmux peut démarrer des shells non-login, qui lisent ~/.bashrc et sautent ~/.profile, car ~/.profile ne s'exécute que pour les shells de login. Un Node par utilisateur via un gestionnaire de versions comme nvm obtient le même résultat ; le but, dans les deux cas, est que npm install -g n'ait jamais besoin de sudo. npm fonctionne toujours très bien, ou utilisez le script d'installation natif d'Anthropic, la valeur par défaut actuellement documentée. Vérifiez la documentation d'installation d'Anthropic avant de coller, les méthodes d'installation changent.
Lancez claude à l'intérieur d'un dépôt pour le démarrer. Le premier lancement vous guide dans l'authentification ; une machine sans écran n'a pas de navigateur, donc le processus vous remet une URL à ouvrir sur votre propre machine et un code à rapporter dans le terminal. (Une clé d'API dans l'environnement est l'autre voie.) Dans les deux cas, cet identifiant vit désormais sur le serveur, ce qui nous amène à la partie que les gens sautent.
La conversation sur le rayon d'impact
Un agent avec accès au shell est un shell. Il peut lire tout ce que l'utilisateur sous lequel il tourne peut lire, et pousser partout où cet utilisateur peut pousser. Ce n'est pas une critique de l'outil, c'est sa définition même, et c'est pourquoi le compte sous lequel il tourne compte plus que n'importe quel réglage individuel.
- Utilisateur dédié et non privilégié. Pas de groupe
sudo, pas de répertoire personnel partagé avec votre propre compte. - Aucun identifiant de production sur la machine. Pas de
~/.aws/credentialscontenant des clés de prod, pas de.envcopié depuis la production, pas de mot de passe de base de données avec accès en écriture à quoi que ce soit d'important. Donnez à l'agent un identifiant de préproduction ou en lecture seule. - Jetons à portée limitée. Un jeton GitHub à grain fin limité à un seul dépôt ; une clé de déploiement quand l'accès en lecture suffit.
Claude Code fournit une option qui saute entièrement ses demandes de permission. Sur un portable, sur un projet jetable, c'est votre choix. Sur un serveur contenant des jetons, elle supprime la dernière chose qui se dresse entre une instruction mal lue et un git push --force. Ce que cette option change réellement, et comment contenir un agent qui tourne avec elle, du bac à sable intégré jusqu'à un VPS jetable, est traité dans faire tourner Claude Code en toute sécurité sur un serveur.
Clé de déploiement ou transfert d'agent SSH
Il est tentant de se connecter avec ssh -A pour que git puisse utiliser la clé de votre portable. Comprenez ce que cela accorde : le transfert d'agent expose le socket de votre agent SSH local aux processus qui tournent sous cet utilisateur sur la machine. Tout ce qui tourne sous agent, y compris l'agent, peut demander à votre clé de signer pour n'importe quel hôte qu'il peut atteindre, tant que vous restez connecté. C'est bien plus que « laisser git récupérer ce seul dépôt ».
Générez plutôt une clé sur le serveur, enregistrez-la comme clé de déploiement par dépôt (accès en écriture seulement si l'agent a besoin de pousser), et définissez une identité git pour que les commits venant de la machine soient reconnaissables :
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"Le flux de travail tmux
Installez-le (sudo apt install tmux), puis un ~/.tmux.conf minimal :
set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"Quatre commandes couvrent l'usage quotidien :
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 claudetmux new -A -s claude est celle à mémoriser : elle attache la session si elle existe et la crée sinon, donc une seule commande couvre à la fois le début de journée et la reprise après une coupure. Faites-en un alias. À l'intérieur d'une session, Ctrl-b c ouvre une fenêtre, Ctrl-b n et Ctrl-b p les font défiler, et Ctrl-b [ entre en mode copie pour remonter dans l'historique (q pour sortir).
Modes de panne
« Ma session a disparu. » tmux ls affiche no server running on /tmp/tmux-1000/default. Cela signifie presque toujours que le processus n'a jamais été à l'intérieur de tmux : vous vous êtes connecté en SSH, avez lancé claude directement, et la déconnexion l'a tué. Rien à récupérer. L'habitude qui l'évite : tmux new -A -s <project> est la première commande après chaque connexion.
Le panneau se réduit à une toute petite boîte. tmux dimensionne une session sur le plus petit client attaché, donc un client obsolète toujours attaché depuis une autre machine comprime l'affichage. Forcez la déconnexion des autres au moment où vous vous attachez : tmux attach -d -t claude.
Une compilation affiche Killed. Un seul mot, pas de trace de pile. Confirmez avec sudo dmesg -T | grep -i -E 'out of memory|killed process', le tueur OOM du noyau a choisi le plus gros processus. Depuis Node vous pouvez plutôt voir FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Les corrections, dans l'ordre : ajouter du swap (ci-dessus), plafonner le parallélisme des tests et du compilateur, augmenter le tas de Node avec NODE_OPTIONS=--max-old-space-size=..., ou monter en gamme le VPS. Le tueur OOM peut aussi choisir le serveur tmux plutôt que la compilation, emportant votre session avec lui ; si systemd-oomd est actif, il peut tuer toute une tranche utilisateur avec le même effet.
npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Une installation globale dans un préfixe appartenant à root. Utilisez le préfixe ~/.npm-global ci-dessus. Si vous avez déjà lancé sudo npm à un moment, vous pouvez aussi voir Your cache folder contains root-owned files, réparez avec sudo chown -R $(id -u):$(id -g) ~/.npm.
claude: command not found, mais seulement parfois. Votre export de PATH se trouve dans ~/.bashrc sous la garde « If not running interactively, don't do anything », donc les shells non interactifs le sautent. Déplacez l'export au-dessus de cette garde et gardez-le dans ~/.bashrc, pas dans ~/.profile : tmux peut démarrer des shells non-login, qui lisent ~/.bashrc et ne touchent jamais ~/.profile.
Couleurs brouillées après l'attachement. Une incompatibilité de TERM, la ligne default-terminal ci-dessus est la solution.
Les sessions disparaissent après un redémarrage. Ce n'est pas un bug : le serveur tmux est un processus, et un redémarrage y met fin. Vérifiez uptime.
Ce qui casse à mesure que ça grandit
Plus de projets. Une session tmux par dépôt, nommée d'après lui ; tmux ls devient alors votre tableau de bord. Sautez la discipline de nommage et vous obtenez des sessions 0, 1, 2. Les ports s'éparpillent de la même façon, six dépôts voulant tous :3000 est le moment d'arrêter de les attribuer à la main et de laisser un proxy inverse Traefik router plusieurs applications sous Docker Compose faire la répartition par nom d'hôte.
Plus de personnes. Les sockets tmux sont propres à chaque utilisateur, donc deux développeurs sur la même machine obtiennent chacun leur propre serveur tmux et ne peuvent pas voir les sessions de l'autre. Partager une seule session via un socket partagé signifie que tout le monde tape dans le même shell en tant que même utilisateur Unix, avec les conséquences d'audit et de permission que cela implique. Des utilisateurs séparés sont la réponse ennuyeuse et correcte.
Travail sans surveillance. tmux est fait pour les sessions interactives auxquelles vous vous attachez. Les tâches qui s'exécutent selon un calendrier sans personne pour surveiller ont leur place dans une unité et un minuteur systemd, où elles obtiennent la journalisation, une politique de redémarrage et la survie au redémarrage gratuitement. Recourir à tmux pour lancer une tâche en forme de cron est un signe que la tâche veut être un service.
Une dernière remarque : liez les serveurs de développement que l'agent démarre à 127.0.0.1, pas à 0.0.0.0, et atteignez-les via un tunnel SSH (ssh -L 3000:127.0.0.1:3000 agent@your-server) plutôt qu'en ouvrant des ports dans ufw. Dès que vous transférez une demi-douzaine de ports, ou qu'un téléphone et un portable veulent tous deux le même aperçu, placez plutôt devant eux un VPN WireGuard auto-hébergé sur le VPS : les serveurs de développement se lient à une interface privée, et ufw continue de tout refuser depuis l'interface publique. Le pare-feu n'aide que si vous arrêtez d'y percer des trous.
Claude Code n'est pas le seul choix : faire tourner un agent IA de codage sur un VPS pèse aussi Aider et Goose.
FAQ
Claude Code continue-t-il de tourner après une coupure de ma connexion SSH ?
Seulement si vous l'avez démarré à l'intérieur de tmux. Un processus lancé directement depuis le shell SSH est un enfant de ce shell et meurt avec le pty quand le lien tombe. À l'intérieur de tmux, le shell appartient au serveur tmux détaché, donc l'agent continue de travailler en pleine tâche et tmux attach vous ramène dans le même historique d'affichage. Faites de tmux new -A -s <project> la première commande après chaque connexion et le problème disparaît.
Dois-je installer la CLI avec sudo npm install -g ?
Non. Un préfixe global appartenant à root vous vaut des erreurs EACCES lors des installations ultérieures et des fichiers appartenant à root dans le cache npm. Réglez le préfixe de npm sur ~/.npm-global (ou utilisez un gestionnaire de versions comme nvm), installez en tant qu'utilisateur non privilégié agent, et exportez ~/.npm-global/bin dans le PATH depuis ~/.bashrc, au-dessus de la garde interactive. Si vous avez déjà lancé sudo npm une fois, réparez le cache avec sudo chown -R $(id -u):$(id -g) ~/.npm.
Le transfert d'agent ssh -A est-il sûr sur une machine qui fait tourner un agent ?
Il accorde bien plus que ce dont la tâche a besoin. Le transfert expose le socket de votre agent SSH local à chaque processus qui tourne sous cet utilisateur, donc n'importe quoi sur la machine peut demander à votre clé de signer pour n'importe quel hôte qu'il peut atteindre tant que vous restez attaché. Générez une clé ed25519 sur le serveur et enregistrez-la comme clé de déploiement par dépôt, avec accès en écriture seulement si l'agent doit réellement pousser.
Pourquoi ma compilation affiche-t-elle seulement Killed ?
Un seul mot sans trace de pile, c'est le tueur OOM du noyau. Confirmez-le avec sudo dmesg -T | grep -i -E 'out of memory|killed process' ; depuis Node vous pouvez voir JavaScript heap out of memory à la place. Parcourez les corrections dans l'ordre : ajoutez un fichier de swap, plafonnez le parallélisme des tests et du compilateur, augmentez NODE_OPTIONS=--max-old-space-size=..., puis montez en gamme le VPS. Attention, le tueur OOM peut choisir le serveur tmux plutôt que la compilation, emportant toute votre session avec lui.
tmux ou un service systemd ?
tmux convient aux sessions interactives auxquelles vous vous attachez, que vous regardez et dans lesquelles vous tapez, ce qui est exactement ce qu'est une session d'agent. Le travail qui s'exécute selon un calendrier sans personne pour surveiller a sa place dans une unité et un minuteur systemd, où la journalisation, une politique de redémarrage et la survie au redémarrage viennent gratuitement. Si vous recourez à tmux pour lancer une tâche en forme de cron, la tâche veut être un service.