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
- Why a coding agent changes the question
- Node and pnpm, or the battle for PATH
- Installing Codex and Claude Code
- Not trusting all of /home/ubuntu
- Bubblewrap, Codex's sandbox
- What about Claude Code?
- Traps to avoid
- If I started over today: the recommended path
- What's next
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:
~/.sshis readable unless explicitly denied. - Secrets inside a project, a
.envfor 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
ubuntuuser, with its tokens. --yolobypasses 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.
If I started over today: the recommended path
Verified on October 30, 2026 on Ubuntu 26.04.1 LTS, with zsh. Replace
~/.zshrc with ~/.bashrc if you use 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 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@latestalternative works, but under nvm it has the same flaw as Claude Code installed with npm.) The script sets upPNPM_HOMEin the shell file; if you're starting from an install older than pnpm 11,pnpm setupfixes thePATHline. The expected result in~/.zshrc:export PNPM_HOME="$HOME/.local/share/pnpm" export PATH="$PNPM_HOME/bin:$PATH" -
Bubblewrap, before Codex's first launch (0.11.1):
sudo apt install bubblewrap -
Codex CLI (0.166.0 at the time of the test):
pnpm add -g @openai/codexThen 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.comExpected results: the first file is created, the second is denied (
Read-only file system),~/.sshstays readable, andcurlgets no connection (printed code:000). To clean up afterwards:rm -f ~/codex-sbx-outside; rm -rf /tmp/codex-sbx -
Claude Code with the native installer, independent of nvm:
curl -fsSL https://claude.ai/install.sh | bash claude doctorIf
claudecan'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). -
A dedicated work zone, and trust only there:
mkdir -p ~/projects# ~/.codex/config.toml [projects."/home/ubuntu/projects"] trust_level = "trusted"Replace
ubuntuwith your user: the path must be absolute (seeecho $HOME/projects). -
Launch the agents from the repository, never from
$HOME:cd ~/projects/<repo>first, thencodexorclaude. 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).
-
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.