Installer Codex et Claude Code sur un VPS sans leur confier tout /home
Installer Codex et Claude Code sur mon VPS a pris dix minutes. Décider où ils avaient le droit d'écrire m'a pris beaucoup plus longtemps. Retour d'expérience sur pnpm, une zone ~/projects de confiance, Bubblewrap, et un sandbox que j'ai finalement laissé éteint.
Par Nicolas Cousin — Publié le 3 novembre 2026
Installer Codex et Claude Code sur un VPS sans leur confier tout /home
TL;DR
J'ai installé Node (via nvm), pnpm, Codex CLI et Claude Code sur mon VPS
Ubuntu. L'installation elle-même tient en quelques commandes, à deux
incidents pnpm près, réglés par une ligne de PATH. La vraie question
était ailleurs : un agent de code écrit des fichiers, lance des commandes,
build, teste et touche à git. Je n'ai donc pas fait confiance à tout
/home/ubuntu, seulement à un répertoire dédié, ~/projects (côté Codex ;
côté Claude Code, c'est une convention). Côté Codex,
j'ai installé Bubblewrap pour que son sandbox utilise le binaire du système,
puis, en écrivant cet article, vérifié ce que ce sandbox bloque vraiment
(les écritures hors du projet, le réseau) et ce qu'il laisse passer (la
lecture de ~/.ssh).
Côté Claude Code, j'ai essayé d'activer son propre sandbox en écrivant cet
article : Ubuntu l'en empêche, et le correctif proposé lèverait une
protection du système. Je l'ai laissé éteint. Ce n'est pas une prison,
c'est du moindre privilège, et je dis plus bas où ça s'arrête.
Sommaire
- Le décor
- Pourquoi un agent de code change la question
- Node et pnpm, ou la bataille du PATH
- Installer Codex et Claude Code
- Ne pas faire confiance à tout /home/ubuntu
- Bubblewrap, le sandbox de Codex
- Et Claude Code ?
- Les pièges à éviter
- Si je recommençais aujourd'hui : le chemin recommandé
- Et ensuite
Le décor
Un VPS Ubuntu sans GPU, durci avant toute installation, avec Ollama et Hermes dans Docker. Dans la répartition que je décrivais au début de la série, les modèles locaux absorbent ce qui peut être lent et asynchrone, et les agents cloud, Claude Code et Codex, prennent le relais pour ce qui est interactif ou doit aboutir vite.
Cet article raconte l'installation de ces deux agents cloud sur le VPS. Chronologiquement, elle a eu lieu avant l'épisode du projet non monté dans Hermes, publié le 23 octobre. C'est un tutoriel, mais pas un tutoriel définitif : je montre ce que j'ai fait, ce qui a coincé, et ce que je ferais autrement aujourd'hui. Les versions citées sont celles relevées sur le VPS le 30 octobre 2026, sauf mention contraire.
Pourquoi un agent de code change la question
Un chatbot, c'est LLM → texte. Si la réponse est mauvaise, je ne la
copie pas, et l'affaire s'arrête là.
Un agent de code moderne ressemble plutôt à ça :
LLM → filesystem → shell → build → tests → git
Il lit et écrit des fichiers, lance des commandes, exécute un build, fait tourner les tests, prépare un commit. Chaque flèche est une action réelle sur la machine, exécutée avec les droits de l'utilisateur qui l'a lancé. La question « est-ce que le modèle est bon ? » passe donc au second plan derrière une autre : où a-t-il le droit d'écrire, et qu'est-ce qu'il peut lire en chemin ? Tout le reste de l'article découle de là.
Node et pnpm, ou la bataille du PATH
Ce que j'ai fait. nvm pour gérer Node, puis pnpm, puis Codex installé
en global avec pnpm. Sur le VPS, aujourd'hui : Ubuntu 26.04.1 LTS, nvm
0.40.3, Node 24.21.0, pnpm 12.4.1. Mon shell de connexion est zsh, donc
tout ce qui touche au PATH vit dans ~/.zshrc (chez vous, ce sera peut-être
~/.bashrc).
Ce qui a coincé. Deux erreurs, à des moments différents.
La première :
ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH
pnpm refuse un pnpm add -g quand le répertoire où il dépose les binaires
globaux n'est pas dans le PATH. C'est un garde-fou plutôt qu'un bug : sans
lui, l'installation réussirait et la commande resterait introuvable. Le
piège, chez moi, vient d'un changement de pnpm 11 : les binaires globaux
sont désormais rangés dans $PNPM_HOME/bin, et plus directement dans
$PNPM_HOME comme avant
(notes de version de pnpm v11.0.0).
Une ancienne ligne PATH qui pointait sur $PNPM_HOME ne suffit plus.
Ma configuration finale, dans ~/.zshrc :
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME/bin:$PATH"
Pour vérifier quel répertoire pnpm attend dans le PATH :
pnpm bin -g
Et pour ne pas écrire ces lignes à la main, pnpm setup les ajoute
lui-même au fichier de configuration du shell, ou les corrige après une
mise à jour vers pnpm 11 ou plus.
La seconde :
Text file busy
C'est ETXTBSY, une erreur du noyau : on tente d'écrire dans un fichier
exécutable pendant qu'il est en cours d'exécution
(man open(2)). Je n'ai
pas noté exactement ce qui tournait à ce moment-là. La cause la plus
probable, c'est une mise à jour ou une réinstallation d'une CLI alors
qu'une session de cette même CLI tournait encore, par exemple dans un
autre panneau tmux. La règle pratique que j'en garde : fermer les sessions
d'un outil avant de le mettre à jour.
Installer Codex et Claude Code
Codex. Installé et configuré le 13 septembre 2026, une fois le PATH
en ordre :
pnpm add -g @openai/codex
codex --version
Installé en 0.154.0, il a été mis à jour depuis : le test de sandbox plus bas a été fait le 30 octobre 2026 avec la 0.166.0.
Claude Code. Je l'ai installé autrement, et je le regrette un peu :
avec npm en global, sous nvm, et pas avec pnpm. Testé avec la version
2.1.269 au moment de l'installation, il est aujourd'hui en 2.1.314, mis à
jour automatiquement.
Ce que je ferais aujourd'hui, c'est suivre la recommandation actuelle
d'Anthropic : l'installeur natif, qui installe dans ~/.local/bin et se
met à jour tout seul.
curl -fsSL https://claude.ai/install.sh | bash
claude doctor
L'installation via npm reste prise en charge, à condition de ne jamais la
lancer avec sudo. Mais avec nvm, elle a un défaut discret : le binaire
claude vit dans le répertoire de la version de Node active. Changer de
version de Node avec nvm peut suffire à le faire disparaître du PATH.
Pour un outil qui n'a rien à voir avec le Node de mes projets, je
préfère qu'il n'en dépende pas.
Ne pas faire confiance à tout /home/ubuntu
Au premier lancement dans un répertoire, Codex peut proposer de lui faire
confiance (c'est ce qu'il m'a demandé) ; sinon il démarre en lecture seule,
et /permissions permet de changer. La tentation est de répondre oui depuis $HOME et de ne plus
y penser.
L'idée que j'ai écartée. Faire confiance à tout /home/ubuntu. Sur ce
VPS, $HOME contient .ssh, .hermes (la configuration et les jetons
d'Hermes), d'autres jetons, des configurations sensibles. Rien de tout ça
n'a sa place dans le périmètre de travail d'un agent de code.
Ce que j'ai retenu. Une zone dédiée, ~/projects/, où vivent les
dépôts sur lesquels je fais travailler les agents, et rien d'autre. Le
moindre privilège, appliqué aux agents de code. Côté Codex, la
configuration réellement en place dans ~/.codex/config.toml est
minimale :
[projects."/home/ubuntu/projects"]
trust_level = "trusted"
Je n'ai réglé ni sandbox_mode ni approval_policy : ce sont les valeurs
par défaut qui s'appliquent (le test plus bas force ce mode explicitement).
D'après
la documentation de Codex,
le mode recommandé pour un dossier de confiance versionné est « Auto » :
workspace-write avec des approbations on-request. Codex y écrit
librement dans l'espace de travail et demande avant de sortir de ce cadre.
Je précise tout de suite le niveau de preuve : cette configuration est en place depuis septembre, mais je ne m'en suis jamais servi pour du vrai travail sur le VPS. La base d'état de Codex n'y enregistre aucune session. Configuré, donc, mais pas éprouvé. Je ne sais même pas si son sandbox s'était déjà déclenché une seule fois avant que je le teste pour cet article.
Un détail à connaître : la confiance s'applique à toute l'arborescence.
Faire confiance à ~/projects, c'est faire confiance à chaque dépôt qu'il
contient, y compris celui qu'on vient de cloner sans l'avoir lu.
Bubblewrap, le sandbox de Codex
Le jour de l'installation, au premier lancement, Codex m'a affiché un avertissement :
Codex could not find bubblewrap on PATH
Ce n'est pas une erreur bloquante. Sur Linux, le sandbox de Codex s'appuie
sur Bubblewrap : il utilise le
premier bwrap trouvé dans le PATH, et à défaut, un utilitaire embarqué
(documentation du sandboxing).
La documentation précise que cet utilitaire embarqué a
besoin de pouvoir créer des user namespaces non privilégiés, ce qu'Ubuntu
restreint justement : sur Ubuntu, installer le paquet n'est pas qu'une
préférence. J'ai donc installé celui du système, dans la foulée :
sudo apt install bubblewrap
bwrap --version # bubblewrap 0.11.1 au 30/10/2026
Il se retrouve dans /usr/bin/bwrap et fonctionne sans privilège
particulier, ce qui compte pour la suite.
Ce qu'il est censé protéger. D'après la documentation, en
workspace-write, les écritures sont limitées à l'espace de travail. Le
réseau est coupé par défaut. Et à l'intérieur même des racines
inscriptibles, .git, .codex, .agents (et .aws) restent en lecture
seule, récursivement :
l'agent ne réécrit pas l'historique ni sa propre configuration sans
passer par une approbation.
Ce que j'ai vérifié. Comme je n'avais jamais utilisé Codex pour de
vrai, je ne pouvais pas me contenter de la documentation. codex sandbox
exécute une commande sous le même sandbox Linux que Codex utilise, sans
appeler le modèle. Le 30 octobre 2026, avec la 0.166.0, dans un dossier
temporaire /tmp/codex-sbx :
codex sandbox -c sandbox_mode="workspace-write" -- <commande>
| Commande | Résultat |
|---|---|
touch ./inside (dans le dossier de travail) |
fichier créé |
touch "$HOME/codex-sbx-outside" |
refusé : Read-only file system |
test -r ~/.ssh && ls ~/.ssh |
lisible, la liste des fichiers s'affiche |
curl https://example.com |
bloqué, pas de connexion |
Les écritures hors du projet et le réseau sont bien bloqués. La lecture de
~/.ssh, elle, passe sans difficulté. C'est exactement la limite qui
suit.
Ce que ça ne protège pas. C'est la partie que j'aurais aimé lire avant de commencer :
- Bubblewrap limite surtout les écritures. Les lectures restent
larges :
~/.sshest lisible, sauf refus explicite. - Les secrets dans un projet, un
.envpar exemple, sont lisibles, et donc susceptibles de partir chez le fournisseur du modèle avec le reste du contexte. - L'agent tourne toujours sous l'utilisateur
ubuntu, avec ses jetons. --yolocontourne tout. L'option existe, il suffit de ne pas la taper.
Pour aller plus loin, il faut un utilisateur Unix dédié ou un conteneur rootless. C'est précisément là que la série va, dans l'arc consacré aux Dev Containers rootless.
Et Claude Code ?
Claude Code tourne chez moi en mode de permission auto : les lectures
et les modifications dans le répertoire de travail sont approuvées
d'office ; le reste passe par un classificateur, qui peut bloquer.
Je dois être honnête sur deux points. D'abord, je n'avais jamais activé le
sandbox de Claude Code sur ce VPS. Ensuite, j'ai lancé au moins une
session en dehors de ~/projects. La zone projects/ est donc une
convention doublée de moindre privilège, pas une cage étanche.
Ce que j'ai essayé, en écrivant cet article. Activer ce sandbox. Sur
Linux, il repose lui aussi sur Bubblewrap, plus socat. J'ai installé
socat, puis choisi dans /sandbox l'option « Sandbox commands, with
auto-allow ». Premier test, un simple touch ./test-sandbox. La commande
a échoué avant même de s'exécuter :
apply-seccomp: write /proc/self/setgroups (nested userns is capability-restricted; caller must provide CAP_SYS_ADMIN): Permission denied
Pourquoi ça a échoué. Depuis la 24.04 (ici 26.04), Ubuntu restreint
les user namespaces non privilégiés via AppArmor
(kernel.apparmor_restrict_unprivileged_userns = 1). Pour que Bubblewrap
continue de fonctionner, Ubuntu fournit un profil,
/etc/apparmor.d/bwrap-userns-restrict : le profil bwrap autorise
bwrap à créer son namespace, mais tout ce que bwrap lance bascule dans
un profil unpriv_bwrap, qui refuse les capabilities (audit deny capability).
Le sandbox de Codex n'a besoin que de ce premier namespace : il fonctionne. Celui de Claude Code lance, à l'intérieur, un utilitaire qui a besoin d'une capability pour installer son filtre seccomp : refusé.
Le correctif proposé, et pourquoi je ne l'ai pas appliqué. La documentation d'Anthropic propose un profil AppArmor de la forme :
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
…
}
Il porte le même nom que le profil d'Ubuntu, et il le remplace (dans
un autre fichier, /etc/apparmor.d/bwrap, mais sous le même nom de
profil ; la doc précise qu'il ne s'applique qu'à bwrap, mais sans
unpriv_bwrap, plus rien ne retire les capabilities à ce que bwrap
lance). Avec
lui, n'importe quel programme pourrait de nouveau obtenir, via bwrap, un
user namespace avec ses capabilities, ce qu'Ubuntu retire volontairement
pour réduire la surface d'attaque du noyau. Lever une protection du
système pour activer le sandbox d'un outil, sur une machine où tournent
justement des agents, c'est un échange que je ne trouve pas bon.
Ma décision. J'ai gardé la protection d'Ubuntu et remis Claude Code en
« No Sandbox », c'est-à-dire sa configuration habituelle : mode auto et
permissions. L'isolation forte, je la renvoie à l'arc consacré aux Dev Containers
rootless.
Un dernier point à connaître si vous l'activez quand même : par défaut,
quand le sandbox de Claude Code ne peut pas démarrer, les commandes
s'exécutent hors sandbox. Pour qu'il échoue franchement à la place, il
faut sandbox.failIfUnavailable: true dans les réglages.
Les pièges à éviter
Je suis tombé dans deux d'entre eux (npm sous nvm, et très probablement la mise à jour en pleine session) ; les autres, je les ai évités.
Répondre « oui » à la confiance depuis $HOME. Ça prend une seconde, et
ça met .ssh, .hermes et tous les jetons dans le périmètre de
l'agent.
Installer Claude Code via npm sous nvm. Ça marche, jusqu'au jour où un
nvm use fait disparaître la commande.
Mettre à jour une CLI pendant qu'une de ses sessions tourne dans un autre
panneau tmux. C'est, très probablement, mon Text file busy.
Confondre « sandbox » et « isolation ». Bubblewrap borne les écritures ; il ne rend pas les secrets illisibles et ne change pas l'utilisateur sous lequel tourne l'agent.
Copier un profil AppArmor depuis une documentation sans regarder quel profil il remplace.
Si je recommençais aujourd'hui : le chemin recommandé
Vérifié le 30 octobre 2026 sur Ubuntu 26.04.1 LTS, avec zsh. Remplacez
~/.zshrc par ~/.bashrc si vous êtes en bash.
-
Node via nvm (0.40.3, Node 24.21.0) :
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash source ~/.zshrc nvm install 24 -
pnpm (12.4.1) via le script autonome, qui ne dépend pas de la version de Node active :
curl -fsSL https://get.pnpm.io/install.sh | sh - source ~/.zshrc pnpm bin -g # ce répertoire doit être dans le PATH(L'alternative
npm install -g pnpm@latestfonctionne, mais sous nvm elle a le même défaut que Claude Code installé avec npm.) Le script configurePNPM_HOMEdans le fichier du shell ; si vous partez d'une installation antérieure à pnpm 11,pnpm setupcorrige la lignePATH. Le résultat attendu dans~/.zshrc:export PNPM_HOME="$HOME/.local/share/pnpm" export PATH="$PNPM_HOME/bin:$PATH" -
Bubblewrap, avant le premier lancement de Codex (0.11.1) :
sudo apt install bubblewrap -
Codex CLI (0.166.0 au moment du test) :
pnpm add -g @openai/codexPuis vérifier le sandbox par vous-même, sans appel au modèle, depuis un dossier de test :
mkdir -p /tmp/codex-sbx && cd /tmp/codex-sbx codex sandbox -c sandbox_mode="workspace-write" -- touch ./inside codex sandbox -c sandbox_mode="workspace-write" -- touch "$HOME/codex-sbx-outside" codex sandbox -c sandbox_mode="workspace-write" -- ls ~/.ssh codex sandbox -c sandbox_mode="workspace-write" -- curl -sS -m 5 -o /dev/null -w '%{http_code}\n' https://example.comRésultats attendus : le premier fichier est créé, le deuxième refusé (
Read-only file system),~/.sshreste lisible, etcurln'obtient pas de connexion (code affiché :000). Pour nettoyer ensuite :rm -f ~/codex-sbx-outside; rm -rf /tmp/codex-sbx -
Claude Code avec l'installeur natif, indépendant de nvm :
curl -fsSL https://claude.ai/install.sh | bash claude doctorSi
claudeest introuvable ensuite :export PATH="$HOME/.local/bin:$PATH"dans~/.zshrc.Ces trois scripts (nvm, pnpm, Claude Code) s'exécutent avec vos droits : lisez-les avant (
curl -fsSL URL | less). -
Une zone de travail dédiée, et la confiance seulement là :
mkdir -p ~/projects# ~/.codex/config.toml [projects."/home/ubuntu/projects"] trust_level = "trusted"Remplacez
ubuntupar votre utilisateur : le chemin doit être absolu (cf.echo $HOME/projects). -
Lancer les agents depuis le dépôt, jamais depuis
$HOME:cd ~/projects/<depot>d'abord,codexouclaudeensuite. Au premier lancement, chaque CLI demande de se connecter (compte ChatGPT pour Codex, compte Claude pour Claude Code).Côté Claude Code, sur Ubuntu 24.04 et plus, la seule borne est le mode de permission et le répertoire de lancement : pas de sandbox du système (voir plus haut).
-
Fermer les sessions avant toute mise à jour d'une de ces CLI.
Et garder en tête la limite : avec le sandbox de Codex, ce chemin réduit ce que l'agent peut abîmer, pas ce qu'il peut lire. Pas de secrets dans les projets, et pour une vraie frontière, un utilisateur dédié ou un conteneur rootless.
Et ensuite
Prochain article : retour à Hermes. Maintenant que le projet est enfin monté dans son conteneur, je rejoue les benchmarks que l'environnement avait faussés, Qwen et Devstral compris. Plus loin dans la série, l'arc consacré aux Dev Containers rootless reprendra la question laissée ouverte ici : comment donner à un agent une vraie frontière, plutôt qu'une convention bien tenue.