Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions

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

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 : ~/.ssh est lisible, sauf refus explicite.
  • Les secrets dans un projet, un .env par 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.
  • --yolo contourne 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.

  1. 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
    
  2. 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@latest fonctionne, mais sous nvm elle a le même défaut que Claude Code installé avec npm.) Le script configure PNPM_HOME dans le fichier du shell ; si vous partez d'une installation antérieure à pnpm 11, pnpm setup corrige la ligne PATH. Le résultat attendu dans ~/.zshrc :

    export PNPM_HOME="$HOME/.local/share/pnpm"
    export PATH="$PNPM_HOME/bin:$PATH"
    
  3. Bubblewrap, avant le premier lancement de Codex (0.11.1) :

    sudo apt install bubblewrap
    
  4. Codex CLI (0.166.0 au moment du test) :

    pnpm add -g @openai/codex
    

    Puis 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.com
    

    Résultats attendus : le premier fichier est créé, le deuxième refusé (Read-only file system), ~/.ssh reste lisible, et curl n'obtient pas de connexion (code affiché : 000). Pour nettoyer ensuite :

    rm -f ~/codex-sbx-outside; rm -rf /tmp/codex-sbx
    
  5. Claude Code avec l'installeur natif, indépendant de nvm :

    curl -fsSL https://claude.ai/install.sh | bash
    claude doctor
    

    Si claude est 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).

  6. 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 ubuntu par votre utilisateur : le chemin doit être absolu (cf. echo $HOME/projects).

  7. Lancer les agents depuis le dépôt, jamais depuis $HOME : cd ~/projects/<depot> d'abord, codex ou claude ensuite. 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).

  8. 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.