SSD Nodes Learn
How to do am Matt ConnorBy Matt Connor · Updated 2026-07-24

how to run Claude Code for VPS with tmux

Stop losing Claude Code sessions when SSH disconnect. Use tmux for Linux VPS so agent processes no go die when SIGHUP hit your terminal session.

Problem na laptop lid, no be CLI

Claude Code dey run fine for your laptop until you close am: SSH session go die, shell go get SIGHUP, and agent wey dey run test for three minutes go die join. Run the CLI for machine wey no dey sleep, inside terminal multiplexer wey processes no be children of your SSH session. Na that one be the trick — tmux, no be installation, na him be the main thing wey dey hold am.

This page na about how to operate box wey you go leave agents dey run for. If you no get Linux server wey you fit leave ON, none of this matter apply. Na that one be the only real prerequisite.

Wetin tmux dey do

When you SSH enter, sshd fork one shell and give am pseudo-terminal; everything wey you start from that shell na child of am. If you drop connection, kernel go tear down the pty, shell go get SIGHUP, and e go hang up on all its children. Any long-running foreground process go die.

tmux change how ownership dey work. The tmux command wey you type na thin client wey dey talk over unix socket to one tmux server wey dey run detached from your terminal. Shells inside one session na children of that server, no be of sshd. If you kill SSH connection, the client go vanish but the server, the session, and the agent go still dey run mid-task. If you reconnect, tmux attach, you go back enter the same shell with the same scrollback. nohup go survive hangup too, but e no go give you way to enter again — you no fit re-attach to one backgrounded TUI. Claude Code dey interactive; tmux (or screen) na the right tool.

Sizing the box

The CLI na Node process; no be the CLI dey fill machine. Wetin dey fill machine na anything wey the agent dey run for your side: build, full test suite, tsc, language server, or database inside Docker. Size the machine for the toolchain, no be for the CLI. Add swap even if you no plan to use am — e dey turn hard OOM kill to slow build:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Watch the disk too: repos, node_modules, and Docker images dey grow fast. And if the toolchain pass container and reach full virtual machines — KVM guest, or local Kubernetes node — check if the plan get CPU virtualisation extensions before you pay, because running nested virtualization on a VPS na something provider dey enable for you, no be something wey you fit switch on from inside the guest.

Use non-root user first

Create one special user wey get him own home, and put your public key inside:

sudo adduser --disabled-password --gecos "" agent
sudo install -d -m 700 -o agent -g agent /home/agent/.ssh
sudo cp ~/.ssh/authorized_keys /home/agent/.ssh/authorized_keys
sudo chown agent:agent /home/agent/.ssh/authorized_keys
sudo chmod 600 /home/agent/.ssh/authorized_keys

We purposely no put agent inside sudo group. If you need any system package, you go install am. That one decision go stop most ways wey one stray shell command fit spoil the host.

SSH hygiene for a box you leave running

To use password auth for machine wey dey public internet all day, wey hold agent and your source code, no be risk wey worth to carry. Turn am off. For Ubuntu 24.04 and Debian 13, /etc/ssh/sshd_config include /etc/ssh/sshd_config.d/*.conf, so just drop one file instead of to edit main config:

# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

Check and reload — keep your current session open while you test one new session from another terminal:

sudo sshd -t && sudo systemctl restart ssh

Small thing for Ubuntu 24.04: sshd dey use socket-activation. Auth settings go work for systemctl restart ssh, but if you change how Port dey listen, you go need systemctl daemon-reload and restart ssh.socket.

Next na the firewall. Allow SSH before you enable am, otherwise you go lock yourself out:

sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

Install fail2ban with clear mind about wetin e dey do: once password auth don off, brute force no fit work anyway — e just dey keep failed attempts out of your journal.

# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1h

Finally, use sudo apt install unattended-upgrades and sudo dpkg-reconfigure -plow unattended-upgrades do automatic patch. Watch out for how e work with tmux: if you switch on Unattended-Upgrade::Automatic-Reboot and kernel update reboot the box, all your session go die. Leave am off and reboot when you want, when nothing dey run.

Install Node.js and Claude Code for Ubuntu

Claude Code na Node CLI, so you need current Node. Distro package dey often slow; NodeSource na the usual way for Ubuntu and Debian, and dem dey ship signed repo (no apt-key — that tool don go):

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --version

Now, na dis part many people dey fail: install the CLI as your agent user, no use sudo npm -g ever. If you use root-owned global prefix, permission errors go start later and root-owned files go dey inside npm cache. Point npm's prefix to the user's home first:

mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g @anthropic-ai/claude-code
claude --version

The export dey go inside ~/.bashrc, no be ~/.profile, and e must dey above the "If not running interactively, don't do anything" guard wey dey near top of the file: tmux fit start non-login shells, wey go read ~/.bashrc and skip ~/.profile~/.profile go only run for login shells. You fit use per-user Node via version manager like nvm to do the same thing; the goal na make sure say npm install -g no need sudo. npm still dey work fine, or you fit use Anthropic's native install script, wey na the current default for documentation. Check Anthropic's install docs before you paste — install methods dey change.

Run claude inside one repo to start am. The first time you run am, e go guide you through authentication; headless box no get browser, so the flow go give you URL wey you go open for your own machine and one code wey you go bring back to the terminal. (You fit also use API key for environment.) Either way, that credential go dey live for the server — wey go carry us go the part wey many people dey skip.

The blast radius conversation

Agent wey get shell access na shell. E fit read anything wey the user wey e dey run as fit read, and e fit push anywhere wey that user fit push. We no dey criticize the tool, na just how e be — and na why the account wey e dey run under dey matter pass any single setting.

  • Dedicated, unprivileged user. No sudo group, no home directory wey share with your own account.
  • No production credentials on the box. No ~/.aws/credentials wey hold prod keys, no .env wey copy from production, no database password wey get write access to anything wey important. Give the agent staging or read-only credential.
  • Scoped tokens. Fine-grained GitHub token wey limit to one repository; deploy key if read access alone dey suffice.

Claude Code carry flag wey go skip all permission prompts. For laptop, or for project wey you no go use again, na your choice. But for server wey hold tokens, that flag remove the last thing wey dey stand between one wrong instruction and git push --force. Wetin the flag really change, and how to control agent wey run with am, from the built-in sandbox up to disposable VPS, dem explain for how to run Claude Code safely on a server.

Deploy key vs SSH agent forwarding

E easy to ssh -A so git fit use the key for your laptop. But make you understand wetin that one dey grant: agent forwarding dey expose your local SSH agent socket to processes wey dey run as that user for the box. Anything wey dey run as agent — including the agent — fit ask your key to sign for any host wey e fit reach, as long as you still dey connect. That one far pass "make git pull this one repo".

Instead, generate key on the server, register am as per-repository deploy key (only write access if the agent need to push), and set git identity so that commits from the box go easy to recognize:

ssh-keygen -t ed25519 -C "agent deploy key" -f ~/.ssh/id_ed25519_repo
cat ~/.ssh/id_ed25519_repo.pub   # paste into the repo's Deploy Keys
git config --global user.name "Agent (build box)"
git config --global user.email "agent@example.com"

The tmux workflow

Install am (sudo apt install tmux), den use dis minimal ~/.tmux.conf:

set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"

Four commands cover wetin you go dey do everyday:

tmux new -A -s claude     # attach to session "claude", creating it if absent
# ...run `claude` inside it, work normally...
# Ctrl-b then d           -> detach; everything keeps running
tmux ls                   # list sessions
tmux attach -t claude     # reattach, from this machine or any other
tmux kill-session -t claude

tmux new -A -s claude na di one wey you must sabi — e go attach di session if e dey, or e go create new one if e no dey. Dis mean one command fit do both starting work and resuming after connection drop. Make you set alias for am. Inside session, Ctrl-b c go open window, Ctrl-b n and Ctrl-b p go dey switch between dem, and Ctrl-b [ go enter copy mode so you fit scroll back (q go exit am).

One thing wey you must know about session wey you no dey kill: di agent dey re-send di whole conversation every turn. Read wey di Claude Code session dey spend im tokens on before you leave one running for one week.

Failure modes

"My session is gone." tmux ls prints no server running on /tmp/tmux-1000/default. This almost always mean say the process never enter tmux — you SSH'd in, run claude directly, and the disconnect kill am. Nothing to recover. The habit wey go stop this thing: make tmux new -A -s <project> be the first command wey you run after every login.

The pane shrink to tiny box. tmux dey size session based on the smallest client wey dey attached, so if one old client still dey attached from another machine, e go squeeze the display. Force dem off as you dey attach: tmux attach -d -t claude.

A build print Killed. One word, no stack trace. Confirm with sudo dmesg -T | grep -i -E 'out of memory|killed process' — the kernel OOM killer pick the biggest process. From Node, you fit see FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory instead. Fixes, for order: add swap (see above), cap test and compiler parallelism, increase Node's heap with NODE_OPTIONS=--max-old-space-size=..., or upgrade the VPS. The OOM killer fit also pick the tmux server instead of the build, and e go carry your session go; if systemd-oomd dey run, e fit kill whole user slice with the same effect.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Global install into a root-owned prefix. Use the ~/.npm-global prefix wey dey above. If you don run sudo npm before, you fit see Your cache folder contains root-owned files too — repair am with sudo chown -R $(id -u):$(id -g) ~/.npm.

claude: command not found — but only sometimes. Your PATH export dey inside ~/.bashrc below the "If not running interactively, don't do anything" guard, so non-interactive shells go skip am. Move the export go above that guard and keep am for ~/.bashrc, no be ~/.profile: tmux fit start non-login shells, wey go read ~/.bashrc and no touch ~/.profile.

Garbled colours after attaching. TERM mismatch — the default-terminal line wey dey above na the fix.

Sessions vanish after a reboot. No be bug: the tmux server na process, and reboot go end am. Check uptime.

Wetin go break as things dey grow

More projects. Make you use one tmux session for every repo, and use the repo name for the session; tmux ls go become your dashboard. If you no follow this naming rule, you go get sessions like 0, 1, and 2. Port wahala go start similarly — if six repos all want use :3000, na time to stop to dey assign them manually and make a Traefik reverse proxy route multiple apps under Docker Compose dey do the work by hostname.

More people. tmux sockets dey belong to one user only, so two developers wey dey use the same box go get their own tmux server and dem no go fit see each other sessions. If you share one session through one shared socket, everyone go dey type for the same shell as the same Unix user, and that one go carry audit and permission wahala. To use separate users na the correct way.

Unattended work. tmux na for interactive sessions wey you dey join. Jobs wey dey run for schedule and nobody dey watch dem no suppose dey tmux; dem suppose dey systemd unit and timer, so dem go get logging, restart policy, and dem go fit start again after boot for free. If you dey use tmux to run cron-shaped job, e mean say the job want to become service.

One last thing: bind dev servers wey the agent start to 127.0.0.1, no be 0.0.0.0, and reach dem through SSH tunnel (ssh -L 3000:127.0.0.1:3000 agent@your-server) instead of to dey open ports for ufw. Once you don start to forward many ports, or if phone and laptop both want use the same preview, put self-hosted WireGuard VPN on the VPS for front of dem: the dev servers go bind to private interface, and ufw go continue to block everything from the public one. Firewall go only help if you stop to dey punch holes for am.

Claude Code no be the only choice: running a coding AI agent on a VPS weigh Aider and Goose too.

FAQ

Claude Code go still dey run if my SSH connection drop?

Only if you start am inside tmux. If you start process straight from SSH shell, e be child of that shell and e go die when pty finish. Inside tmux, the shell belong to the detached tmux server, so the agent go still dey work mid-task and tmux attach go bring you back to the same scrollback. Make tmux new -A -s <project> be your first command after every login and the problem go finish.

I suppose install the CLI with sudo npm install -g?

No. If you use root-owned global prefix, you go get EACCES errors for later installs and root-owned files for npm cache. Set npm prefix to ~/.npm-global (or use version manager like nvm), install am as the unprivileged agent user, and export ~/.npm-global/bin onto PATH from ~/.bashrc, above the interactive guard. If you don already run sudo npm once, use sudo chown -R $(id -u):$(id -g) ~/.npm to repair the cache.

ssh -A agent forwarding safe for box wey dey run agent?

E dey give more permission than the job need. Forwarding dey expose your local SSH agent socket to every process wey dey run as that user, so anything for the box fit use your key sign for any host wey e fit reach as long as you dey attached. Generate ed25519 key for the server and register am as per-repository deploy key, with write access only if the agent really need to push.

Why my build dey print only Killed?

One word without stack trace mean say na kernel OOM killer. Confirm am with sudo dmesg -T | grep -i -E 'out of memory|killed process'; from Node, you fit see JavaScript heap out of memory instead. Follow these fixes one by one: add swapfile, cap test and compiler parallelism, increase NODE_OPTIONS=--max-old-space-size=..., then upgrade the VPS size. Watch out because OOM killer fit pick tmux server instead of the build, and e go carry your whole session go.

tmux or systemd service?

tmux dey work for interactive sessions wey you dey attach to, watch, and type for, and na exactly wetin agent session be. Work wey dey run for schedule without person to watch belong for systemd unit and timer, because logging, restart policy, and boot survival dey come free for there. If you dey use tmux to run cron-shaped job, that job suppose be service.