Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions

Installing Codex and Claude Code on a VPS without handing them all of /home

Installing Codex and Claude Code on my VPS took ten minutes. Deciding where they were allowed to write took me much longer. A field report on pnpm, a trusted ~/projects zone, Bubblewrap, and a sandbox I ended up leaving off.

By Nicolas Cousin — Published on November 3, 2026

Installing Codex and Claude Code on a VPS without handing them all of /home

TL;DR

I installed Node (via nvm), pnpm, Codex CLI and Claude Code on my Ubuntu VPS. The installation itself fits in a few commands, give or take two pnpm incidents, fixed with one PATH line. The real question was elsewhere: a coding agent writes files, runs commands, builds, tests and touches git. So I didn't trust all of /home/ubuntu, only a dedicated directory, ~/projects (on the Codex side; for Claude Code it's a convention). On the Codex side, I installed Bubblewrap so its sandbox would use the system binary, then, while writing this article, checked what that sandbox really blocks (writes outside the project, the network) and what it lets through (reading ~/.ssh). On the Claude Code side, I tried to enable its own sandbox while writing this article: Ubuntu prevents it, and the suggested fix would lift a system protection. I left it off. This isn't a jail, it's least privilege, and I explain below where it stops.


Table of contents


The setup

An Ubuntu VPS with no GPU, hardened before anything was installed, running Ollama and Hermes in Docker. In the split I described at the start of the series, local models take on whatever can be slow and asynchronous, and the cloud agents, Claude Code and Codex, take over for anything interactive or that has to get done quickly.

This article covers installing those two cloud agents on the VPS. Chronologically, it happened before the episode of the project that wasn't mounted in Hermes, published on October 23. It's a tutorial, but not a definitive one: I show what I did, what went wrong, and what I'd do differently today. The versions quoted are the ones recorded on the VPS on October 30, 2026, unless stated otherwise.

Why a coding agent changes the question

A chatbot is LLM → text. If the answer is bad, I don't copy it, and that's the end of it.

A modern coding agent looks more like this:

LLM → filesystem → shell → build → tests → git

It reads and writes files, runs commands, runs a build, runs the tests, prepares a commit. Every arrow is a real action on the machine, carried out with the rights of the user who launched it. So the question "is the model any good?" takes a back seat to another one: where is it allowed to write, and what can it read along the way? Everything else in this article follows from that.

Node and pnpm, or the battle for PATH

What I did. nvm to manage Node, then pnpm, then Codex installed globally with pnpm. On the VPS, today: Ubuntu 26.04.1 LTS, nvm 0.40.3, Node 24.21.0, pnpm 12.4.1. My login shell is zsh, so everything touching PATH lives in ~/.zshrc (for you, it may be ~/.bashrc).

What went wrong. Two errors, at different times.

The first one:

ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH

pnpm refuses a pnpm add -g when the directory where it puts global binaries isn't in PATH. It's a safeguard rather than a bug: without it, the install would succeed and the command would still not be found. The trap, in my case, comes from a change in pnpm 11: global binaries are now stored in $PNPM_HOME/bin, no longer directly in $PNPM_HOME as before (pnpm v11.0.0 release notes). An old PATH line pointing at $PNPM_HOME is no longer enough.

My final configuration, in ~/.zshrc:

export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME/bin:$PATH"

To check which directory pnpm expects in PATH:

pnpm bin -g

And to avoid writing these lines by hand, pnpm setup adds them to the shell's configuration file itself, or fixes them after upgrading to pnpm 11 or later.

The second one:

Text file busy

That's ETXTBSY, a kernel error: something tries to write to an executable file while it's running (man open(2)). I didn't note exactly what was running at the time. The most likely cause is an update or reinstall of a CLI while a session of that same CLI was still running, for example in another tmux pane. The practical rule I took from it: close a tool's sessions before updating it.

Installing Codex and Claude Code

Codex. Installed and configured on September 13, 2026, once PATH was sorted out:

pnpm add -g @openai/codex
codex --version

Installed as 0.154.0, it has been updated since: the sandbox test further down was done on October 30, 2026 with 0.166.0.

Claude Code. I installed it differently, and I somewhat regret it: globally with npm, under nvm, rather than with pnpm. Tested with version 2.1.269 at install time, it's now on 2.1.314, updated automatically.

What I'd do today is follow Anthropic's current recommendation: the native installer, which installs into ~/.local/bin and updates itself.

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

Installing via npm is still supported, as long as you never run it with sudo. But with nvm, it has a subtle flaw: the claude binary lives in the directory of the active Node version. Switching Node versions with nvm can be enough to make it vanish from PATH. For a tool that has nothing to do with my projects' Node, I'd rather it didn't depend on it.

Not trusting all of /home/ubuntu

The first time it's launched in a directory, Codex may offer to trust it (that's what it asked me); otherwise it starts read-only, and /permissions lets you change that. The temptation is to say yes from $HOME and never think about it again.

The idea I ruled out. Trusting all of /home/ubuntu. On this VPS, $HOME contains .ssh, .hermes (Hermes's configuration and tokens), other tokens, sensitive configuration. None of that belongs inside a coding agent's working perimeter.

What I went with. A dedicated zone, ~/projects/, holding the repositories I have the agents work on, and nothing else. Least privilege, applied to coding agents. On the Codex side, the configuration actually in place in ~/.codex/config.toml is minimal:

[projects."/home/ubuntu/projects"]
trust_level = "trusted"

I set neither sandbox_mode nor approval_policy: the defaults apply (the test below sets this mode explicitly). According to the Codex documentation, the recommended mode for a trusted, version-controlled folder is "Auto": workspace-write with on-request approvals. Codex writes freely within the workspace and asks before stepping outside it.

Let me be clear right away about the level of evidence: this configuration has been in place since September, but I've never used it for real work on the VPS. Codex's state database there records no sessions. Configured, then, but not battle-tested. I don't even know whether its sandbox had ever kicked in once before I tested it for this article.

One detail worth knowing: trust applies to the whole tree. Trusting ~/projects means trusting every repository in it, including the one you just cloned without reading it.

Bubblewrap, Codex's sandbox

On installation day, at first launch, Codex showed me a warning:

Codex could not find bubblewrap on PATH

It's not a blocking error. On Linux, Codex's sandbox relies on Bubblewrap: it uses the first bwrap found in PATH, and failing that, a bundled helper (sandboxing documentation). The documentation points out that this bundled helper needs to be able to create unprivileged user namespaces, which is exactly what Ubuntu restricts: on Ubuntu, installing the package isn't just a preference. So I installed the system one right away:

sudo apt install bubblewrap
bwrap --version   # bubblewrap 0.11.1 as of 10/30/2026

It ends up in /usr/bin/bwrap and works without any special privilege, which matters for what follows.

What it's supposed to protect. According to the documentation, in workspace-write, writes are limited to the workspace. The network is off by default. And even inside the writable roots, .git, .codex, .agents (and .aws) stay read-only, recursively: the agent doesn't rewrite history or its own configuration without going through an approval.

What I checked. Since I had never actually used Codex, I couldn't just take the documentation's word for it. codex sandbox runs a command under the same Linux sandbox Codex uses, without calling the model. On October 30, 2026, with 0.166.0, in a temporary folder /tmp/codex-sbx:

codex sandbox -c sandbox_mode="workspace-write" -- <command>
Command Result
touch ./inside (in the working folder) file created
touch "$HOME/codex-sbx-outside" denied: Read-only file system
test -r ~/.ssh && ls ~/.ssh readable, the file list is shown
curl https://example.com blocked, no connection

Writes outside the project and the network are indeed blocked. Reading ~/.ssh, on the other hand, goes through without a hitch. That's exactly the limit that comes next.

What it doesn't protect. This is the part I wish I'd read before starting:

  • Bubblewrap mostly restricts writes. Reads stay broad: ~/.ssh is readable unless explicitly denied.
  • Secrets inside a project, a .env for example, are readable, and so may end up at the model provider along with the rest of the context.
  • The agent still runs as the ubuntu user, with its tokens.
  • --yolo bypasses everything. The option exists; just don't type it.

To go further, you need a dedicated Unix user or a rootless container. That's precisely where the series is heading, in the arc on rootless Dev Containers.

What about Claude Code?

On my machine, Claude Code runs in the auto permission mode: reads and edits in the working directory are approved outright; everything else goes through a classifier, which can block it.

I have to be honest about two things. First, I had never enabled Claude Code's sandbox on this VPS. Second, I've launched at least one session outside ~/projects. So the projects/ zone is a convention backed by least privilege, not a watertight cage.

What I tried, while writing this article. Enabling that sandbox. On Linux, it also relies on Bubblewrap, plus socat. I installed socat, then picked the "Sandbox commands, with auto-allow" option in /sandbox. First test, a simple touch ./test-sandbox. The command failed before it even ran:

apply-seccomp: write /proc/self/setgroups (nested userns is capability-restricted; caller must provide CAP_SYS_ADMIN): Permission denied

Why it failed. Since 24.04 (here 26.04), Ubuntu restricts unprivileged user namespaces through AppArmor (kernel.apparmor_restrict_unprivileged_userns = 1). To keep Bubblewrap working, Ubuntu ships a profile, /etc/apparmor.d/bwrap-userns-restrict: the bwrap profile allows bwrap to create its namespace, but everything bwrap launches switches to an unpriv_bwrap profile, which denies capabilities (audit deny capability).

Codex's sandbox only needs that first namespace: it works. Claude Code's sandbox, by contrast, launches a helper inside it that needs a capability to install its seccomp filter: denied.

The suggested fix, and why I didn't apply it. Anthropic's documentation suggests an AppArmor profile along these lines:

profile bwrap /usr/bin/bwrap flags=(unconfined) {
  userns,
  …
}

It has the same name as Ubuntu's profile, and it replaces it (in a different file, /etc/apparmor.d/bwrap, but under the same profile name; the docs point out that it only applies to bwrap, but without unpriv_bwrap, nothing strips capabilities from what bwrap launches anymore). With it, any program could again get, through bwrap, a user namespace that keeps its capabilities, which Ubuntu deliberately strips to reduce the kernel's attack surface. Lifting a system protection to enable one tool's sandbox, on a machine that happens to run agents, is a trade-off I don't think is a good one.

My decision. I kept Ubuntu's protection and set Claude Code back to "No Sandbox", meaning its usual configuration: auto mode and permissions. I'm leaving strong isolation to the rootless Dev Containers arc.

One last thing to know if you enable it anyway: by default, when Claude Code's sandbox can't start, commands run outside the sandbox. To make it fail outright instead, you need sandbox.failIfUnavailable: true in the settings.

Traps to avoid

I fell into two of them (npm under nvm, and most likely the update mid-session); the others I avoided.

Saying "yes" to trust from $HOME. It takes one second, and it puts .ssh, .hermes and every token inside the agent's perimeter.

Installing Claude Code via npm under nvm. It works, until the day an nvm use makes the command disappear.

Updating a CLI while one of its sessions is running in another tmux pane. That's most likely what caused my Text file busy.

Confusing "sandbox" with "isolation". Bubblewrap bounds writes; it doesn't make secrets unreadable and doesn't change the user the agent runs as.

Copying an AppArmor profile from documentation without checking which profile it replaces.

Verified on October 30, 2026 on Ubuntu 26.04.1 LTS, with zsh. Replace ~/.zshrc with ~/.bashrc if you use 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 the standalone script, which doesn't depend on the active Node version:

    curl -fsSL https://get.pnpm.io/install.sh | sh -
    source ~/.zshrc
    pnpm bin -g    # this directory must be in PATH
    

    (The npm install -g pnpm@latest alternative works, but under nvm it has the same flaw as Claude Code installed with npm.) The script sets up PNPM_HOME in the shell file; if you're starting from an install older than pnpm 11, pnpm setup fixes the PATH line. The expected result in ~/.zshrc:

    export PNPM_HOME="$HOME/.local/share/pnpm"
    export PATH="$PNPM_HOME/bin:$PATH"
    
  3. Bubblewrap, before Codex's first launch (0.11.1):

    sudo apt install bubblewrap
    
  4. Codex CLI (0.166.0 at the time of the test):

    pnpm add -g @openai/codex
    

    Then check the sandbox for yourself, without calling the model, from a test folder:

    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
    

    Expected results: the first file is created, the second is denied (Read-only file system), ~/.ssh stays readable, and curl gets no connection (printed code: 000). To clean up afterwards:

    rm -f ~/codex-sbx-outside; rm -rf /tmp/codex-sbx
    
  5. Claude Code with the native installer, independent of nvm:

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

    If claude can't be found afterwards: export PATH="$HOME/.local/bin:$PATH" in ~/.zshrc.

    These three scripts (nvm, pnpm, Claude Code) run with your rights: read them first (curl -fsSL URL | less).

  6. A dedicated work zone, and trust only there:

    mkdir -p ~/projects
    
    # ~/.codex/config.toml
    [projects."/home/ubuntu/projects"]
    trust_level = "trusted"
    

    Replace ubuntu with your user: the path must be absolute (see echo $HOME/projects).

  7. Launch the agents from the repository, never from $HOME: cd ~/projects/<repo> first, then codex or claude. On first launch, each CLI asks you to sign in (ChatGPT account for Codex, Claude account for Claude Code).

    On the Claude Code side, on Ubuntu 24.04 and later, the only bound is the permission mode and the launch directory: no system sandbox (see above).

  8. Close sessions before any update of one of these CLIs.

And keep the limit in mind: with Codex's sandbox, this path reduces what the agent can damage, not what it can read. No secrets in projects, and for a real boundary, a dedicated user or a rootless container.

What's next

Next article: back to Hermes. Now that the project is finally mounted in its container, I'm rerunning the benchmarks the environment had skewed, Qwen and Devstral included. Further on in the series, the arc on rootless Dev Containers will pick up the question left open here: how to give an agent a real boundary, rather than a well-kept convention.