You want Claude Code on an Ubuntu box without fighting Node versions, sudo npm or a sandbox that refuses to start. Since the CLI now ships as a single native binary, you can install Claude Code on Ubuntu with one curl line and no Node.js at all. The apt repository and npm still exist, and each one behaves a little differently on 26.04, which is what this guide is about.
We cover all three install paths, then auth, a sane settings.json, a project CLAUDE.md and an MCP server. We also reproduced the /sandbox warning that blames the AppArmor user-namespace restriction in Ubuntu, and tested the fixes that clear it. I ran through all of it again on a fresh Ubuntu 26.04 server VM in October 2026 with Claude Code 2.1.312, and every output below comes from that box.
Prerequisites
Our lab box was a 2 vCPU, 4 GB Proxmox VM with a regular sudo user called kibet. Anthropic lists 4 GB of RAM as the floor, and that held up fine for the CLI plus one MCP server. You need:
- Ubuntu 26.04 LTS (24.04 and 22.04 take the same installer)
- A sudo user and at least 4 GB RAM
- A paid Claude plan (Pro, Max, Team, Enterprise) or a Console API key with billing
- Outbound HTTPS to
claude.ai,platform.claude.com,downloads.claude.aiandapi.anthropic.com registry.npmjs.orgas well for the npm path and npx MCP servers
You cannot use the free Claude.ai plan here, because it does not include Claude Code, so sort out the plan or the API key before you start. Everything up to the auth step works without one, so you can prepare a box and hand it over to the person who holds the account. That is how the lab run went: every capture before Step 3 came from a VM with no Claude login at all, which shows that neither the installers nor claude doctor need credentials to do their job.
Step 1: Install Claude Code with the native installer
The native installer is the method Anthropic recommends, and it is the one we would pick on any workstation. It drops a self-contained binary under ~/.local/share/claude/versions, points ~/.local/bin/claude at it and keeps it updated in the background. There is no Node.js, no npm and no root involved. Run it as your normal user:
curl -fsSL https://claude.ai/install.sh | bash
On a brand new user the script finishes with a PATH note, because ~/.local/bin did not exist when the shell started:
✔ Claude Code successfully installed!
Version: 2.1.312
Location: ~/.local/bin/claude
Next: Run claude --help to get started
⚠ Setup notes:
● Native installation exists but ~/.local/bin is not in your PATH. Run:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
This is not a broken install. The default ~/.profile on Ubuntu adds ~/.local/bin at login only when the directory already exists, so the shell that ran the installer simply started too early to see it. A new login fixes it by itself, which is why the note is easy to ignore on a desktop. If you want the command in the current shell right now, add the line the installer suggests:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Now confirm the binary runs and ask it to check itself. claude doctor is read-only and does not start a session, so it works before you have signed in:
claude --version
claude doctor
The lines that matter are Running: native, Auto-updates: enabled and the last one. We left out the Remote Control and organization-policy lines that an unsigned box adds, because they only say you have not logged in yet and are not install problems:
2.1.312 (Claude Code)
Claude Code doctor
Running: native (2.1.312)
Commit: f375cd85d4f7
Platform: linux-x64
Path: /home/kibet/.local/share/claude/versions/2.1.312
Config install method: native
Search: OK (bundled)
Auto-updates: enabled
Auto-update channel: latest
Last update attempt: none recorded
No installation issues found.
The screenshot below is the same run on the lab VM, from the install script through to the doctor checkup:

From here claude works from any directory and the background updater keeps it on the latest channel. If your updates go through the system package manager instead, Step 2 covers apt and npm.
Step 2: Install Claude Code with apt or npm instead
The two other paths matter on servers and fleets where every package goes through one tool. They install the same CLI, but they do not update the same way, and on our box they did not even land on the same build. This is how the three compared on the same VM on the same morning:
- Native installer: newest build, updates itself, lives in
~/.local - apt
stable: a few builds behind, updates with apt - npm: newest build, needs Node.js 22+, updates with npm
Our pick depends on the machine. On a laptop or a single dev VM, use the native installer, because it stays current without you thinking about it. On a server managed by Ansible or Landscape, use apt, because the version is pinned by the repo and changes only when you run an upgrade. npm only makes sense if your team already pins tools through a Node toolchain.
Signed apt repository
Anthropic publishes a GPG-signed apt repository, which is the cleanest choice when Ansible or unattended-upgrades already manage your packages. Add the key and the stable repo, then install:
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL https://downloads.claude.ai/keys/claude-code.asc -o /etc/apt/keyrings/claude-code.asc
echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main" | sudo tee /etc/apt/sources.list.d/claude-code.list
sudo apt update
sudo apt install -y claude-code
The signed-by option means apt will trust anything signed with that key for this one repository, so the key itself deserves a check before the first apt update pulls packages with it. Compare its fingerprint with the value in the Anthropic setup docs:
gpg --show-keys /etc/apt/keyrings/claude-code.asc
Our copy of the key reads 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE, which matches the published value:
pub rsa4096 2026-03-30 [SCE]
31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
uid Anthropic Claude Code Release Signing <[email protected]>
Then check which build apt actually installed. The install log does not say which release you got, and the channel decides it, so this is the only quick way to know whether the box matches the version your team tested:
apt-cache policy claude-code | head -3
/usr/bin/claude --version
On the same morning that the native installer pulled the newest build, the stable channel sat a few releases behind it:
claude-code:
Installed: 2.1.308-1
Candidate: 2.1.308-1
2.1.308 (Claude Code)
That gap is by design. The stable channel trails the tip by about a week and skips builds with known regressions, which is what you want on a shared server. If you would rather ride the newest build, use apt/latest latest main in the source line instead. Note that apt installs never auto-update, so claude doctor on this binary reports Auto-updates: Managed by package manager and you upgrade with sudo apt update && sudo apt upgrade claude-code.
npm (for existing Node setups)
The npm package @anthropic-ai/claude-code needs Node.js 22 or newer. The nodejs package in the Ubuntu 26.04 archive is new enough, so there is no need for NodeSource on this release (the Node.js on Ubuntu 26.04 guide covers the other options):
sudo apt install -y nodejs npm
node --version
You should see a 22.x release from the archive build:
v22.22.1
Do not install the package with sudo npm install -g. Anthropic advises against it, and with the npm from the Ubuntu archive the default global prefix is /usr/local, which your user cannot write to. Give npm a prefix in your home directory first, so every global package lives under your user and later upgrades never need root. The PATH line matters as much as the prefix, because without it the shell cannot find the new claude binary:
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
npm install -g @anthropic-ai/claude-code
npm fetches the same native binary through a per-platform dependency, so the result is the newest build again, and doctor names the method:
added 3 packages in 16s
Running: npm-global (2.1.312)
Path: /home/kibet/.npm-global/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe
Upgrade an npm install with npm install -g @anthropic-ai/claude-code@latest, not npm update -g, which sticks to the original semver range. Pick one method per user and stay with it, because the CLI notices when two of them live side by side (see the errors section).
Step 3: Pick an auth path
These are the auth backends most Ubuntu installs use (the docs also cover Microsoft Foundry). The right one depends on who pays for the tokens:
| Backend | Best for | Setup |
|---|---|---|
| Anthropic account (OAuth) | Pro, Max and Team subscribers | Browser flow via claude auth login |
| API key | Console pay-per-token, CI runners | ANTHROPIC_API_KEY env var |
| Amazon Bedrock | AWS-hosted Claude billing | CLAUDE_CODE_USE_BEDROCK=1 plus AWS creds |
| Google Vertex AI | GCP-hosted Claude billing | CLAUDE_CODE_USE_VERTEX=1 plus ADC |
Path A: OAuth login (Anthropic account)
Sign in once. The command prints a URL and waits for the code you get back from the browser:
claude auth login
Open the URL on any machine, sign in, and paste the code back into the terminal. The token is stored under ~/.claude and reused on every later launch. claude auth status prints the state as JSON, and on our fresh box it read "loggedIn": false with "authMethod": "none" until the login was done.
Path B: API key (best for servers)
Headless servers and CI runners should never need a browser. Create a key in the Claude Console at platform.claude.com/settings/keys and export it:
echo 'export ANTHROPIC_API_KEY="your-anthropic-api-key-here"' >> ~/.bashrc
source ~/.bashrc
Then start claude interactively once. It finds the key and asks whether to use it, and the highlighted default is No (recommended), so pressing Enter out of habit throws the key away. Move up to Yes first:
Detected a custom API key in your environment
Do you want to use this API key?
Yes
❯ No (recommended)
Enter to confirm · Esc to cancel
Once the key is approved, claude --print "say hello in one sentence" (alias -p) is the quickest auth test. It runs without the TUI, prints the reply to stdout and exits, which is also the building block for scripting Claude into cron jobs and pipelines.
Step 4: Start your first interactive session
Open the REPL inside a project directory, because that directory becomes the workspace Claude may read and edit:
mkdir -p ~/projects/my-app
cd ~/projects/my-app
claude
On the first launch you get a theme picker, a short security note and a folder trust prompt. Read that last one before you press anything. Its default is No, exit, and we closed our own session on the lab box by hitting Enter through the screens. Pick Yes, I trust this folder and the prompt appears:
▐▛███▛█ Claude Code v2.1.312
▝▜██████▀ Opus 5.5 · API Usage Billing
▝▝ ▝▝ ~/projects/my-app
❯ Try "create a util logging.py that..."
⏸ manual mode on · ? for shortcuts · ← for agents
The banner tells you two useful things. The model name shows what this account defaults to (an API key gave us Opus, while subscription seats can default to a different model), and manual mode on means Claude asks before it runs a tool or edits a file. Keep it that way until you have written the permission rules in Step 5. A good first prompt:
What does this repo do? Walk me through the main entry points.
Claude reads the README, the manifest (package.json, pyproject.toml, Cargo.toml) and the file tree, then answers in the terminal. /help lists the slash commands, /model switches models, and Ctrl+C twice exits.
Step 5: Configure settings.json and CLAUDE.md
Two files turn the CLI into an agent you can trust on a real machine. ~/.claude/settings.json holds your global preferences (model, permissions, sandbox, hooks), and CLAUDE.md holds the rules for one project. Create the settings file:
mkdir -p ~/.claude
vim ~/.claude/settings.json
This is the file we ran on the lab box. It pins the sonnet alias, draws a line around what the agent may run, and turns on the bash sandbox:
{
"model": "sonnet",
"permissions": {
"allow": ["Bash(git:*)", "Bash(npm:*)", "Bash(pytest:*)", "Read", "Edit", "Write"],
"deny": ["Bash(rm -rf:*)"]
},
"sandbox": {
"enabled": true
}
}
allow lists tool patterns that skip the prompt, and deny lists patterns Claude refuses even if you approve them later. They are prefix matches, so Bash(rm -rf:*) catches rm -rf but not rm -fr. That is why the sandbox block is there too: with sandbox.enabled set, bash commands run inside bubblewrap with filesystem and network limits, which covers what a prefix rule misses. On Linux the sandbox needs two packages:
sudo apt install -y bubblewrap socat
One side effect to know about: the sandbox defaults to auto-allow, so sandboxed Bash commands stop asking for approval even in manual mode, while your deny rules still apply. The panel calls this “Sandbox commands, with auto-allow”, and you can switch it to regular permissions there. Lock the file to your user with chmod 600 ~/.claude/settings.json once hooks or MCP tokens land in it. Run claude doctor after any edit, since it validates the settings file and reports a bad key there instead of failing at session start. Next, type /sandbox in a session. If its Dependencies tab shows an AppArmor message, jump to the sandbox error below; a stock 26.04 server opened straight into the Mode tab for us. Now give the project its own rules:
vim CLAUDE.md
Keep it short and specific. Every line is loaded into context on every session, so a long file costs tokens each time and vague advice gets ignored. Commands, limits and stack facts work best:
# Project context
Stack: FastAPI + Postgres + Redis behind Caddy.
Run tests with pytest -x. Lint with ruff check .
Keep changes under 200 lines per PR. Prefer dataclasses over dicts.
Claude loads this at session start and keeps it in context. The Claude Code hooks guide adds checks that run around every tool call, and the sub-agents guide shows how to fan work out once one session is not enough.
Step 6: Add an MCP server
MCP servers let Claude read live data from your stack: a database, an API, a docs folder. claude mcp add takes a name and then the launch command after a -- separator. The filesystem server is the simplest one to prove the wiring. It runs through npx, so on the native or apt path install Node first with sudo apt install -y nodejs npm, or mcp list will report the server as failed:
claude mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem $HOME/projects
claude mcp list
mcp add writes to the local config for the current project, and mcp list starts each server to check it, so a green Connected means the command really launches:
Added stdio MCP server filesystem with command: npx -y @modelcontextprotocol/server-filesystem /home/kibet/projects to local config
File modified: /home/kibet/.claude.json [project: /home/kibet/projects/my-app]
Checking MCP server health…
filesystem: npx -y @modelcontextprotocol/server-filesystem /home/kibet/projects - ✔ Connected
Because the server was added to the local config, it only loads when you start Claude inside ~/projects/my-app, so another project on the same box does not inherit it. Remove it with claude mcp remove filesystem. For real work, swap it for the Postgres or GitHub servers from the MCP servers setup guide, or package your own tools with the plugins guide.
Step 7: Learn the flags you will use first
Of the dozens of flags, these are the ones we reach for on day one, checked against claude --help on the lab build:
| Flag | What it does |
|---|---|
-p, --print | Print the reply and exit (scripts, cron) |
-c, --continue | Continue the most recent conversation in this directory |
--add-dir DIR | Allow tool access to more directories |
--model opus | Model for this session (aliases such as fable, opus, sonnet, or a full name) |
--permission-mode MODE | manual, acceptEdits, auto, plan, dontAsk or bypassPermissions |
--dangerously-skip-permissions | Bypass every permission check; throwaway VMs only |
--mcp-config PATH | Load extra MCP servers from a JSON file |
--settings FILE | Load a settings file or inline JSON for this run |
--bare | Minimal mode for CI: skips hooks, plugins, auto-memory, CLAUDE.md and MCP servers; Anthropic API key or apiKeyHelper, no subscription login |
Combined, they make a cheap one-shot task. This one has a small model read a week of commits and draft a changelog. Note that --bare ignores subscription logins, so it needs ANTHROPIC_API_KEY set in the shell that runs it:
cd ~/projects/site && claude --bare --model haiku --print "review the diff from the last 7 days and write a CHANGELOG entry"
We ran nothing like this from cron in the lab, so treat a scheduled version as your own job to wire up. Cron reads neither ~/.bashrc nor ~/.profile, which means the crontab line has to call the binary by its full path in your home directory (ours was /home/kibet/.local/bin/claude) and set the API key itself, for example by sourcing a 0600 env file. You will find the full slash-command and flag reference in the Claude Code cheat sheet, and the cheat sheets hub has the printable PDF versions.
Common errors and what they mean
The first three errors below came up on the 26.04 lab box during this run, with the exact text the CLI printed. The last one, an expired login, needs a real account, so we took its wording from the official error reference.
Sandbox: “Ubuntu’s AppArmor restriction on user namespaces stops bubblewrap here”
Ubuntu restricts unprivileged user namespaces through AppArmor (kernel.apparmor_restrict_unprivileged_userns = 1), and bubblewrap needs one to build the sandbox. On a stock 26.04 server this is already handled: the apparmor package ships /etc/apparmor.d/bwrap-userns-restrict, which grants /usr/bin/bwrap the userns rule. The /sandbox panel was clean on our first test because that profile was loaded. When it is not loaded (we unloaded it to reproduce the case, and the Claude Code docs say 24.04 and later can hit it), bubblewrap fails and /sandbox shows this on its Dependencies tab:
ripgrep (rg): found
bubblewrap (bwrap): installed
socat: installed
seccomp filter: installed
Ubuntu's AppArmor restriction on user namespaces stops bubblewrap here ("loopback: Failed RTM_NEWADDR: Operation not permitted"). Add bubblewrap's
AppArmor profile from https://code.claude.com/docs/en/sandboxing, or run `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` (holds until
reboot), then restart Claude Code
Before touching anything, confirm both halves of the diagnosis, because the fix below only applies to this cause. The sysctl should read 1, the bwrap profiles should be missing from aa-status, and a plain bubblewrap call should fail the same way outside Claude. If you are inside a Docker container rather than on a VM or bare metal, the failure has a different cause, and the sandboxing docs handle that case with enableWeakerNestedSandbox:
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo aa-status | grep -E "^\s+(bwrap|unpriv_bwrap)$"
bwrap --ro-bind / / --unshare-user --dev /dev --proc /proc true
With the profile missing, aa-status prints nothing for bwrap and the last command fails:
kernel.apparmor_restrict_unprivileged_userns = 1
bwrap: setting up uid map: Permission denied
There are three ways out, and we tested all three on the lab VM. In order of preference:
- Reload the bwrap profile that Ubuntu ships
- Add the bwrap profile from the Claude Code docs
- Turn the sysctl off until the next reboot
The first option is the one to use on 26.04, because the file is already on disk and it keeps the restriction on for every other program. It only lets bwrap create the namespace, and it still drops capabilities for the commands bwrap starts:
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict
sudo aa-status | grep -E "^\s+(bwrap|unpriv_bwrap)$"
Both profiles should now be listed, and after a Claude restart /sandbox opens on the Mode tab with no warning:
bwrap
unpriv_bwrap
Our lab VM capture shows the whole sequence, from the blocked bwrap call through the warning to the reloaded profile:

If /etc/apparmor.d/bwrap-userns-restrict does not exist on your system, use the profile from the Claude Code sandboxing docs instead. Do not add it on a box that already has the Ubuntu file, because both files define a profile named bwrap and whichever loads last replaces the other. Open the new file:
sudo vim /etc/apparmor.d/bwrap
Paste the profile exactly as we tested it:
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
include if exists <local/bwrap>
}
Load it with sudo apparmor_parser -r /etc/apparmor.d/bwrap. Loaded on its own, it cleared the warning on our box too. It is the looser of the two, because the Ubuntu profile moves the commands bwrap starts into a separate stacked profile (unpriv_bwrap) that denies capabilities, while this one leaves bwrap unconfined.
The sysctl route in the message, sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0, also cleared the warning in our test. It is the bigger tradeoff, because it lifts the restriction for every program on the machine rather than just bwrap. It also resets at reboot, so it is fine for a quick test on a throwaway VM and the wrong choice for a workstation you keep.
Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules'
This is what npm install -g @anthropic-ai/claude-code printed on 26.04 with the archive npm and no prefix set:
npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /usr/local/lib/node_modules
npm ERR! errno -13
npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules'
Your global prefix is /usr/local, which belongs to root. Do not rerun it with sudo. Set the user prefix from Step 2 and install again, or switch to the native installer, which never touches system directories.
Warning: Leftover npm global installation
We had a native install and an npm install for the same user, and both claude update and claude doctor flagged it:
Warning: Multiple installations found
- npm-global at /home/kibet/.npm-global/bin/claude
- native at /home/kibet/.local/bin/claude (currently running)
Warning: Leftover npm global installation at /home/kibet/.npm-global/bin/claude
Fix: Run: npm -g uninstall @anthropic-ai/claude-code
Whichever binary comes first in PATH wins, so you can end up updating one copy and running the other. Run the suggested npm -g uninstall @anthropic-ai/claude-code. Doctor did not flag the apt copy in /usr/bin on the same box, so check which -a claude yourself if you mixed apt with another method.
Login expired or the wrong credential is in use
We had no real login in the lab, so this one comes from the Claude Code error reference rather than our own capture. An expired subscription login shows up as Login expired · Please run /login or an OAuth token message, and a bad key as API Error: 401 Invalid authentication credentials. Run /login (or claude auth login) to renew, and /status to see which credential the session is actually using, because an exported ANTHROPIC_API_KEY you approved in Step 3 takes over from the subscription login.
Update and uninstall Claude Code
A native install updates itself, but you can force a check at any time:
claude update
It prints the running version, checks the channel and ends on the result. When a second install exists, the warnings from the errors section print between these lines:
Current version: 2.1.312
Checking for updates to latest version...
Claude Code is up to date (2.1.312)
apt installs update with sudo apt update && sudo apt upgrade claude-code, and npm installs with npm install -g @anthropic-ai/claude-code@latest. To remove a native install, delete the launcher, the version store and, if you will not come back, your config:
rm -f ~/.local/bin/claude
rm -rf ~/.local/share/claude
rm -rf ~/.claude ~/.claude.json
An apt install comes off with sudo apt remove claude-code plus the list file in /etc/apt/sources.list.d and the keyring. An npm install comes off with npm -g uninstall @anthropic-ai/claude-code. Deleting ~/.claude also removes settings, MCP definitions and session history, so skip that line if you plan to reinstall.
If you also run other coding agents on the same machine, the OpenCode install guide uses the same Ubuntu box layout, and the AppArmor guide for Ubuntu 26.04 explains the profile modes behind the sandbox fix. The token-reduction guide is the next stop once the monthly bill starts to matter.