SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor

Exécuter Claude Code sur un VPS avec tmux

Faites tourner Claude Code sur un VPS Linux toujours allumé avec tmux : vos sessions survivent aux coupures SSH. Installation, durcissement et erreurs à prévoir.

Le problème vient du capot du portable, pas de la CLI

Claude Code fonctionne correctement sur votre portable jusqu’à ce que vous refermiez le capot : la session SSH se ferme, le shell reçoit un SIGHUP et l’agent s’arrête avec elle trois minutes après le début d’un test. Exécutez la CLI sur une machine qui ne se met jamais en veille, dans un multiplexeur de terminal dont les processus ne sont pas des enfants de votre session SSH. C’est tout le principe. La partie essentielle est tmux, pas l’installation.

Cette page explique comment exploiter une machine sur laquelle vous laissez des agents s’exécuter. Si vous n’avez pas de serveur Linux que vous pouvez laisser allumé, rien de tout cela ne s’applique. C’est le seul prérequis à prendre au sérieux.

Ce que fait réellement tmux

Lorsque vous vous connectez en SSH, sshd lance un shell et lui attribue un pseudo-terminal. Tout ce que vous lancez depuis ce shell en devient un processus fils. Si la connexion est interrompue, le noyau détruit le pty, le shell reçoit SIGHUP, puis transmet à son tour le signal de déconnexion à ses processus fils. Les processus de longue durée exécutés au premier plan s’arrêtent.

tmux inverse cette relation de parenté. La commande tmux que vous saisissez est un client léger qui communique via un socket Unix avec un serveur tmux exécuté indépendamment de votre terminal. Les shells d’une session sont les processus fils de ce serveur, et non de sshd. Si la connexion SSH est interrompue, le client disparaît, tandis que le serveur, la session et l’agent en cours d’exécution continuent de fonctionner. Reconnectez-vous, exécutez tmux attach, et vous retrouvez le même shell avec le même historique de défilement. nohup résiste également à une déconnexion, mais ne permet pas de reprendre la session : vous ne pouvez pas vous rattacher à une interface TUI exécutée en arrière-plan. Claude Code est interactif ; tmux (ou screen) est donc l’outil adapté.

Dimensionnement de la machine

Le CLI lui-même est un binaire natif unique ; ce n’est pas lui qui remplit la machine. Ce qui remplit la machine, c’est ce que l’agent exécute pour votre compte : un build, une suite complète de tests, tsc, un language server ou une base de données dans Docker. Dimensionnez la machine pour la toolchain, pas pour le CLI. Ajoutez du swap même si vous prévoyez de ne jamais l’utiliser : il transforme un kill OOM brutal en build lent :

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

Surveillez également le disque : les dépôts, node_modules et les images Docker s’accumulent rapidement. Si la toolchain va au-delà des conteneurs et utilise des machines virtuelles complètes, par exemple un invité KVM ou un nœud Kubernetes local, vérifiez que l’offre expose les extensions de virtualisation du CPU avant de vous engager. En effet, exécuter une virtualisation imbriquée sur un VPS dépend d’une activation par le fournisseur ; vous ne pouvez pas l’activer depuis l’invité.

D’abord, un utilisateur non-root

Créez un utilisateur dédié avec son propre répertoire personnel, puis installez-y votre clé publique :

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

Testez la connexion depuis un second terminal avant d’aller plus loin, tant que l’authentification par mot de passe reste disponible comme solution de secours. Si le message Permission refusée (publickey) s’affiche, le problème vient généralement du propriétaire ou du mode du répertoire .ssh, et non de la clé elle-même.

Par conception, agent ne fait pas partie du groupe sudo. Si un paquet système est nécessaire, installez-le. Cette seule décision élimine la plupart des situations dans lesquelles une commande shell exécutée par erreur peut endommager l’hôte.

Hygiène SSH pour un serveur qui reste allumé

L’authentification par mot de passe sur une machine connectée en permanence à Internet, qui contient un agent et votre code source, représente un risque inutile. Désactivez-la. Sur Ubuntu 24.04 et Debian 13, /etc/ssh/sshd_config inclut /etc/ssh/sshd_config.d/*.conf. Ajoutez donc un fichier au lieu de modifier la configuration principale :

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

N’ajoutez pas l’échange de clés dans ce fichier, sauf raison précise : une ligne KexAlgorithms copiée depuis un ancien guide de durcissement se limite souvent aux algorithmes classiques. C’est ainsi qu’un client ssh récent finit par afficher l’avertissement indiquant que la connexion n’utilise pas d’échange de clés post-quantique.

Validez la configuration, puis rechargez le service. Gardez votre session actuelle ouverte pendant que vous testez une nouvelle session depuis un second terminal :

sudo sshd -t && sudo systemctl restart ssh

Point particulier sur Ubuntu 24.04 : sshd est activé par socket. Les paramètres d’authentification s’appliquent avec systemctl restart ssh, mais une modification de Port, qui écoute les connexions, nécessite également systemctl daemon-reload et un redémarrage de ssh.socket.

Passez ensuite au pare-feu. Autorisez SSH avant de l’activer, sinon vous vous verrouillez l’accès :

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

Installez fail2ban en sachant précisément ce qu’il apporte : une fois l’authentification par mot de passe désactivée, une attaque par force brute ne peut de toute façon pas aboutir. Cet outil évite que les tentatives échouées remplissent vos journaux.

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

Enfin, appliquez automatiquement les correctifs avec sudo apt install unattended-upgrades et sudo dpkg-reconfigure -plow unattended-upgrades. Tenez compte de l’interaction avec tmux : activez Unattended-Upgrade::Automatic-Reboot et une mise à jour du noyau redémarrera le serveur, ce qui interrompra toutes les sessions. Laissez cette option désactivée et redémarrez selon votre propre planning, lorsqu’aucune tâche n’est en cours d’exécution. La même prudence s’applique à une mise à niveau de version : passer le serveur d’Ubuntu 24.04 à 26.04 redémarre sshd et le noyau. Planifiez donc cette opération pendant une fenêtre où aucune session tmux n’exécute une tâche importante.

Installer Node.js et Claude Code sur Ubuntu

La méthode npm nécessite Node.js 22 ou une version ultérieure. (Le paquet inclut désormais un binaire natif qui ne s’exécute pas sous Node, mais npm vérifie toujours la version du moteur, et la toolchain de l’agent aura généralement besoin d’une version récente de Node.) Le paquet de la distribution est souvent en retard ; NodeSource est la méthode habituelle sur Ubuntu et Debian, et fournit un dépôt signé (sans apt-key, qui est obsolète sur Ubuntu 24.04 et a déjà été supprimé de Debian 13) :

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

Voici l’erreur la plus fréquente : installez la CLI avec votre utilisateur agent, jamais avec sudo npm -g. Un préfixe global appartenant à root provoque ensuite des erreurs de permissions et laisse des fichiers appartenant à root dans le cache npm. Configurez d’abord le préfixe npm dans 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 --version

L’export doit être placé dans ~/.bashrc, au-dessus de la condition « If not running interactively, don't do anything » située près du début du fichier. Une fenêtre tmux standard exécute un login shell : tmux laisse default-command vide, ce qui lui indique de lancer default-shell comme login shell, et un bash de connexion lit ~/.profile, qui, sur Ubuntu, source ~/.bashrc. Les shells qui ne sont pas des login shells, par exemple si vous définissez vous-même default-command, lisent directement ~/.bashrc. Ainsi, ~/.bashrc couvre les deux cas, tandis que ~/.profile seul ne couvrirait pas le second. Attention : bash ignore ~/.profile si ~/.bash_profile ou ~/.bash_login existe. Si vous en avez créé un, faites-le donc aussi sourcer ~/.bashrc. Un Node installé par utilisateur avec un version manager tel que nvm produit le même résultat ; dans les deux cas, l’objectif est que npm install -g n’ait jamais besoin de sudo. npm fonctionne toujours correctement, mais la documentation d’Anthropic indique que le script d’installation natif est la méthode recommandée. Des dépôts apt, dnf et apk signés constituent une autre possibilité. Consultez la documentation d’installation d’Anthropic avant de coller une commande, car les méthodes d’installation changent.

Exécutez claude dans un dépôt pour le démarrer. Au premier lancement, l’outil vous guide pour l’authentification. Une machine headless n’a pas de navigateur : le processus vous fournit donc une URL à ouvrir sur votre propre machine, ainsi qu’un code à reporter dans le terminal. (Une clé API définie dans l’environnement constitue l’autre méthode.) Dans les deux cas, ces identifiants résident désormais sur le serveur. C’est ce qui nous amène à la partie souvent ignorée.

Le périmètre d’impact

Un agent qui dispose d’un accès shell est un shell. Il peut lire tout ce que l’utilisateur sous lequel il s’exécute peut lire, et pousser partout où cet utilisateur peut pousser. Ce n’est pas une critique de l’outil, mais sa définition. C’est pourquoi le compte utilisé compte davantage que n’importe quel paramètre individuel.

  • Utilisateur dédié et non privilégié. Aucun groupe sudo, aucun répertoire personnel partagé avec votre propre compte.
  • Aucun identifiant de production sur la machine. Aucun ~/.aws/credentials contenant des clés de production, aucun .env copié depuis la production, aucun mot de passe de base de données permettant d’écrire dans un système important. Fournissez à l’agent un identifiant de staging ou en lecture seule.
  • Tokens limités. Un token GitHub à granularité fine, limité à un seul dépôt ; une deploy key lorsque l’accès en lecture suffit.

Claude Code fournit le flag --dangerously-skip-permissions, qui ignore presque toutes ses demandes d’autorisation. Sur un laptop ou dans un projet jetable, c’est votre choix. Sur un serveur qui contient des tokens, il supprime le dernier obstacle entre une instruction mal interprétée et un git push --force. Les demandes d’autorisation que vous ignorez ne sont pas non plus entièrement activées ou désactivées. Avec le mode automatique désormais activé par défaut (à partir de Claude Code v2.1.283), il est utile de savoir quel mode d’autorisation appliquer à un serveur que vous ne surveillez pas. Les effets réels de ce flag et les moyens de contenir un agent qui l’utilise, du sandbox intégré jusqu’à un VPS jetable, sont présentés dans exécuter Claude Code en toute sécurité sur un serveur.

Deploy key ou transfert de l’agent SSH

Il peut être tentant de ssh -A afin que git puisse utiliser la clé présente sur votre laptop. Comprenez ce que cela autorise : le transfert de l’agent expose le socket de votre agent SSH local aux processus exécutés sous cet utilisateur sur la machine. Tout processus exécuté 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é. Cela va bien au-delà de « permettre à git de récupérer ce dépôt ».

Générez plutôt une clé sur le serveur, enregistrez-la comme deploy key pour un dépôt donné (avec un accès en écriture uniquement si l’agent doit pousser), puis définissez une identité git afin que les commits effectués depuis la machine soient identifiables :

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 workflow tmux

Installez-le (sudo apt install tmux), puis créez une configuration minimale avec ~/.tmux.conf :

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

Quatre commandes suffisent pour l’utilisation quotidienne :

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 est la commande à retenir : elle se rattache à la session si elle existe et la crée sinon. Une seule commande suffit donc pour commencer la journée et reprendre le travail après une déconnexion. Créez un alias pour cette commande. Dans une session, Ctrl-b c ouvre une fenêtre, Ctrl-b n et Ctrl-b p permettent de passer de l’une à l’autre, et Ctrl-b [ active le mode copie pour remonter dans l’historique (q permet d’en sortir).

Retenez un point concernant les sessions que vous ne supprimez jamais : l’agent renvoie l’intégralité de la conversation à chaque tour. Consultez donc ce qu’une session Claude Code exécutée pendant longtemps consomme en tokens avant d’en laisser une active pendant une semaine.

Modes de défaillance

« Ma session a disparu. » tmux ls affiche no server running on /tmp/tmux-1000/default. Cela signifie presque toujours que le processus n’était pas dans tmux : vous vous êtes connecté en SSH, avez exécuté claude directement, puis la déconnexion l’a arrêté. Il n’y a rien à récupérer. Pour éviter ce problème, exécutez tmux new -A -s <project> en première commande après chaque connexion.

Le pane se réduit à une toute petite fenêtre. L’option window-size détermine quel client attaché définit la taille. Depuis tmux 3.1 (Ubuntu 24.04 fournit la version 3.4), la valeur par défaut est latest, c’est-à-dire le client dont l’activité est la plus récente. En revanche, une configuration contenant window-size smallest utilise la taille du plus petit client attaché. Un ancien client resté attaché depuis une autre machine réduit donc l’affichage. Forcez la déconnexion des autres clients lors de l’attachement avec tmux attach -d -t claude.

Un build affiche Killed. Un seul mot, sans stack trace. Vérifiez avec sudo dmesg -T | grep -i -E 'out of memory|killed process' : le kernel OOM killer a sélectionné le processus le plus gourmand. Avec Node, vous pouvez également voir FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Procédez dans cet ordre : ajoutez du swap (voir plus haut), limitez le parallélisme des tests et du compilateur, augmentez le heap de Node avec NODE_OPTIONS=--max-old-space-size=... ou augmentez la taille du VPS. L’OOM killer peut également sélectionner le serveur tmux au lieu du build, ce qui interrompt aussi votre session. Si systemd-oomd est actif, il peut arrêter toute une slice utilisateur avec le même résultat.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Une installation globale dans un prefix appartenant à root. Utilisez le prefix ~/.npm-global indiqué plus haut. Si vous avez déjà exécuté sudo npm, vous pouvez également voir Your cache folder contains root-owned files. Réparez le problème avec sudo chown -R $(id -u):$(id -g) ~/.npm.

claude: command not found, mais seulement parfois. Votre export PATH se trouve dans ~/.bashrc, sous la protection « If not running interactively, don't do anything ». Les shells non interactifs l’ignorent donc. Déplacez l’export au-dessus de cette protection et conservez-le dans ~/.bashrc : un pane tmux par défaut est un shell de login qui atteint ~/.bashrc via le ~/.profile d’Ubuntu, tandis qu’un shell qui n’est pas un shell de login lit directement ~/.bashrc. Ce fichier couvre donc les deux cas.

Les couleurs sont incorrectes après l’attachement. Il s’agit d’une incompatibilité de TERM. La ligne default-terminal indiquée plus haut corrige le problème.

Les sessions disparaissent après un redémarrage. Ce n’est pas un bug : le serveur tmux est un processus, et un redémarrage l’arrête. Vérifiez uptime.

Ce qui se dégrade avec la croissance

Plus de projets. Utilisez une session tmux par dépôt et nommez-la d’après le dépôt ; tmux ls devient alors votre tableau de bord. Si vous ne suivez pas cette règle de nommage, vous obtenez les sessions 0, 1 et 2. Lorsqu’elles sont plusieurs à fonctionner en parallèle, elles n’ont pas besoin de rester isolées, car une session peut envoyer un message à une autre sur la même machine. C’est utile lorsqu’un agent qui exécute un refactoring long demande à un autre de lancer les tests. Les ports se multiplient de la même manière. Lorsque six dépôts veulent tous utiliser :3000, il est temps d’arrêter de les attribuer manuellement et de laisser un reverse proxy Traefik router plusieurs applications sous Docker Compose en fonction du nom d’hôte.

Plusieurs utilisateurs. Les sockets tmux sont propres à chaque utilisateur. Deux développeurs connectés à la même machine disposent donc chacun de leur propre serveur tmux et ne peuvent pas voir les sessions de l’autre. Partager une session via une socket commune signifie que tout le monde saisit des commandes dans le même shell, avec le même utilisateur Unix, et avec les conséquences que cela implique pour l’audit et les permissions. Des utilisateurs distincts constituent la réponse simple et correcte.

Travail sans supervision. tmux sert aux sessions interactives auxquelles vous vous attachez. Les tâches planifiées qui s’exécutent sans supervision doivent utiliser une unité et un timer systemd. Elles bénéficient ainsi de la journalisation, d’une stratégie de redémarrage et d’une reprise automatique au boot. Utiliser tmux pour exécuter une tâche de type cron indique que cette tâche devrait être un service.

Dernier point : liez les serveurs de développement lancés par l’agent à 127.0.0.1, et non à 0.0.0.0, puis accédez-y via un tunnel SSH (ssh -L 3000:127.0.0.1:3000 agent@your-server) au lieu d’ouvrir des ports dans ufw. Si vous redirigez déjà une demi-douzaine de ports, ou si un téléphone et un ordinateur portable doivent accéder simultanément au même aperçu, placez plutôt un VPN WireGuard auto-hébergé sur le VPS devant ces serveurs : les serveurs de développement se lient à une interface privée et ufw continue de refuser toutes les connexions provenant de l’interface publique. Le pare-feu n’est utile que si vous cessez d’y créer des ouvertures.

Claude Code n’est pas la seule option : exécuter un agent d’IA de programmation sur un VPS permet également d’évaluer Aider et Goose.

FAQ

Claude Code continue-t-il à fonctionner après la coupure de ma connexion SSH ?

Seulement si vous l’avez lancé dans tmux. Un processus lancé directement depuis le shell SSH est un enfant de ce shell et s’arrête avec le pty lorsque la connexion est coupée. Dans tmux, le shell appartient au serveur tmux détaché. L’agent continue donc son travail et tmux attach vous ramène à la même sortie historique. Faites de tmux new -A -s <project> la première commande après chaque connexion. Le problème disparaîtra.

Dois-je installer la CLI avec sudo npm install -g ?

Non. Un prefix global détenu par root provoque des erreurs EACCES lors des installations suivantes et crée des fichiers détenus par root dans le cache npm. Définissez le prefix de npm sur ~/.npm-global (ou utilisez un gestionnaire de versions comme nvm), installez les paquets avec l’utilisateur non privilégié agent, puis ajoutez ~/.npm-global/bin à PATH depuis ~/.bashrc, avant le garde-fou interactif. Si vous avez déjà exécuté sudo npm, corrigez 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 exécute un agent ?

Il accorde bien plus de droits que nécessaire. Le transfert expose le socket de votre agent SSH local à chaque processus exécuté avec cet utilisateur. Tout processus présent sur la machine peut donc demander à votre clé de signer des connexions vers n’importe quel hôte accessible, tant que votre session reste attachée. Générez une clé ed25519 sur le serveur et enregistrez-la comme deploy key pour le dépôt concerné. N’accordez l’accès en écriture que si l’agent doit réellement effectuer des push.

Pourquoi mon build affiche-t-il simplement Killed ?

Un seul mot sans stack trace indique généralement l’action de l’OOM killer 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. Appliquez les correctifs dans l’ordre : ajoutez un swapfile, limitez le parallélisme des tests et du compilateur, augmentez NODE_OPTIONS=--max-old-space-size=..., puis redimensionnez le VPS. Attention : l’OOM killer peut sélectionner le serveur tmux au lieu du build et fermer toute votre session.

tmux ou un service systemd ?

tmux convient aux sessions interactives auxquelles vous vous attachez, que vous surveillez et dans lesquelles vous saisissez des commandes. C’est exactement le cas d’une session d’agent. Une tâche exécutée selon un calendrier, sans personne pour la surveiller, doit être gérée par une unité et un timer systemd. Vous bénéficiez ainsi directement de la journalisation, d’une politique de redémarrage et de la reprise après le boot. Si vous utilisez tmux pour exécuter une tâche de type cron, cette tâche devrait être un service.