Run Claude Code for VPS with tmux make SSH no kill am
Run Claude Code for Linux VPS inside tmux, so SSH drops no go stop your agent. Learn installation, hardening, and wetin SIGHUP and tmux failure fit cause.
Problem na laptop lid, no be CLI
Claude Code dey run well for your laptop until you close am: SSH session go die, shell go get SIGHUP, and agent wey don dey run test for three minutes go die with am. Run the CLI for machine wey no dey sleep, inside terminal multiplexer wey processes no be children of your SSH session. Na the whole trick be that, and tmux, no be installation, na the part wey carry the main load.
This page na about how to operate box wey you go leave agents running on. If you no get Linux server wey you fit leave switched on, none of this one apply. Na that be the one honest prerequisite.
Wetin tmux really dey do
When you SSH in, sshd go create a shell and give am pseudo-terminal; everything wey you start from that shell na im child. If connection drop, kernel go tear down the pty, shell go receive SIGHUP, then e go hang up on im children too. Long-running foreground processes go die.
tmux change this ownership arrangement. The tmux command wey you type na thin client wey dey communicate through a unix socket with a tmux server wey dey run separate from your terminal. Shells inside a session na children of that server, no be children of sshd. If SSH connection die, client go close, but server, session, and agent wey dey work go continue to run. When you connect again, tmux attach, you go return to the same shell with the same scrollback. nohup fit survive hangup too, but e no give you any way to enter am again; you no fit re-attach to a TUI wey you background. Claude Code dey interactive; tmux (or screen) na the correct tool.
Size wey the machine need
The CLI na Node process; e no be wetin dey fill the machine. Na wetin the agent run for your behalf dey fill the machine: a 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 plan say you no go use am. E fit turn hard OOM kill into 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/fstabMonitor the disk too: repos, node_modules and Docker images dey pile up quick. And if the toolchain reach beyond containers enter full virtual machines, like KVM guest or local Kubernetes node, check say the plan expose CPU virtualisation extensions before you commit. Running nested virtualization on a VPS na something wey provider go enable for you, no be something wey you fit switch on from inside the guest.
A root user no be the first option
Create one dedicated user with im own home, then put your public key there:
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_keysTest login from another terminal before you continue, while password auth still dey available as fallback. If e return Permission denied (publickey), the problem usually dey with ownership or mode for that .ssh directory, not the key itself.
By design, agent no dey inside the sudo group. If you need any system package, install am. This one decision remove most ways wey one stray shell command fit damage the host.
SSH hygiene for box wey you leave running
Password auth for machine wey dey public internet all day, and wey dey hold agent plus your source code, no be risk wey worth carrying. Turn am off. For Ubuntu 24.04 and Debian 13, /etc/ssh/sshd_config dey include /etc/ssh/sshd_config.d/*.conf, so put file inside am instead of editing the main config:
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin noValidate and reload am, but leave your current session open while you test a new one from second terminal:
sudo sshd -t && sudo systemctl restart sshOne detail for Ubuntu 24.04: sshd dey use socket activation. Auth settings go apply for systemctl restart ssh, but if you change the listening Port, you still need systemctl daemon-reload and restart ssh.socket.
Then configure the firewall. Allow SSH before you enable am, or you go lock yourself out:
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enableInstall fail2ban with clear understanding of wetin e provide: once password auth don off, brute force no fit succeed anyway. E only keeps failed attempts out of your journal.
# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1hFinally, patch automatically with sudo apt install unattended-upgrades and sudo dpkg-reconfigure -plow unattended-upgrades. Note how e interact with tmux: if you switch on Unattended-Upgrade::Automatic-Reboot, kernel update go reboot the box and carry every session along. Leave am off and reboot by yourself when nothing dey run. The same caution apply to release upgrade: move the box from Ubuntu 24.04 to 26.04 go restart sshd and the kernel, so do am during a maintenance window when no tmux session dey hold work wey matter to you.
Install Node.js and Claude Code for Ubuntu
Claude Code na Node CLI, so you need current Node. Distro package dey often behind; NodeSource na the usual way for Ubuntu and Debian, and e get signed repo (no apt-key, that tool don comot):
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --versionNow na the part wey people dey get wrong: install the CLI as your agent user, never use sudo npm -g. Global prefix wey root own go cause permission errors later and leave files wey root own inside npm cache. First point npm prefix go the user's home:
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 --versionPut the export for ~/.bashrc, no be ~/.profile, and e suppose dey above the "If not running interactively, don't do anything" guard near the top of the file: tmux fit start non-login shells, wey dey read ~/.bashrc and skip ~/.profile, while ~/.profile dey run only for login shells. Per-user Node through version manager like nvm fit achieve the same thing; the goal na make npm install -g never need sudo. npm still dey work well, or you fit use Anthropic's native install script, wey be the default wey documentation currently show. Check Anthropic's install docs before you paste, because install methods dey change.
Run claude inside a repo to start am. The first run go guide you through authentication; headless box no get browser, so the flow go give you URL to open for your own machine and code to bring back to the terminal. (API key for the environment na the other option.) Either way, that credential don dey live for the server now, and na that one bring us to the part wey people dey skip.
The blast radius conversation
Agent wey get shell access na shell. E fit read anything wey the user account wey e dey run as fit read, and e fit push go anywhere wey that user fit push. This no be criticism of the tool. Na the definition of the tool be that. Na why the account wey e dey run under matter pass any single setting.
- Dedicated, unprivileged user. No
sudogroup, no home directory wey you share with your own account. - No production credentials on the box. No
~/.aws/credentialswey dey hold prod keys, no.envwey dem copy down from production, and no database password wey get write access to anything important. Give the agent staging credential or read-only credential. - Scoped tokens. Use fine-grained GitHub token wey limit to one repository; use deploy key when read access dey enough.
Claude Code ships a flag wey dey skip all its permission prompts. For laptop or throwaway project, na your choice. But for server wey dey hold tokens, e remove the last thing wey dey between instruction wey agent misunderstand and a git push --force. The prompts wey you go skip no be all-or-nothing too. With auto mode wey dey come as the new default, e make sense to know which permission mode you suppose pin for server wey you no dey monitor. How to run Claude Code safely on a server cover wetin the flag really change, and how to contain agent wey dey run with am, from the built-in sandbox reach disposable VPS.
Deploy key vs SSH agent forwarding
E dey tempting to ssh -A make git fit use the key for your laptop. Understand wetin this one grant: agent forwarding 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 remain connected. This pass "make git pull this one repo" by far.
Generate key for the server instead. Register am as deploy key for the specific repository. Give am write access only if the agent need push. Then set git identity so commits from the box go dey easy to recognise:
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"tmux workflow
Install am (sudo apt install tmux), then use one minimal ~/.tmux.conf:
set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"Four commands go cover everyday use:
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 claudetmux new -A -s claude na the one you need memorise. E go attach if the session dey exist, and e go create am if e no exist. So one command fit handle both starting work for the day and continuing after connection drop. Create alias for am. Inside session, Ctrl-b c go open window, Ctrl-b n and Ctrl-b p go move between dem, while Ctrl-b [ go enter copy mode so you fit scroll go back (q go exit).
One thing you need know about sessions wey you never kill: the agent dey send the whole conversation again for every turn. So read wetin long-lived Claude Code session dey spend its tokens on before you leave one dey run for one week.
Failure modes
"My session is gone." tmux ls go print no server running on /tmp/tmux-1000/default. Almost always, e never dey inside tmux; you SSH in, run claude directly, and the disconnect kill am. Nothing dey to recover. The habit wey go prevent am: tmux new -A -s <project> na the first command after every login.
The pane shrinks to a tiny box. tmux dey size session based on the smallest attached client, so one stale client wey still attach from another machine fit squeeze the display. Force the other clients comot as you attach: tmux attach -d -t claude.
A build prints Killed. One word, no stack trace. Confirm with sudo dmesg -T | grep -i -E 'out of memory|killed process'; kernel OOM killer don pick the biggest process. From Node, you fit instead see FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Fixes, in order: add swap (above), limit test and compiler parallelism, increase Node heap with NODE_OPTIONS=--max-old-space-size=..., or upgrade the VPS size. OOM killer fit also pick the tmux server instead of the build, and carry your session along; if systemd-oomd dey run, e fit kill one whole user slice with the same result.
npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Na global install enter prefix wey root own. Use the ~/.npm-global prefix above. If you don run sudo npm before, you fit also see Your cache folder contains root-owned files; 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 dey skip am. Move the export above that guard and keep am for ~/.bashrc, no be ~/.profile: tmux fit start non-login shells, wey dey read ~/.bashrc and never touch ~/.profile.
Garbled colours after attaching. Na TERM mismatch; the default-terminal line above na the fix.
Sessions vanish after a reboot. No be bug: tmux server na process, and reboot go end am. Check uptime.
Wetin go break as e dey grow
More projects. Make one tmux session dey for each repo, and name am after the repo; tmux ls go then be your dashboard. If you no follow the naming discipline, you go get sessions 0, 1, 2. When several sessions dey run at once, dem no need work alone, because one session fit send message go another one for the same box, and this dey useful when the agent wey dey handle long refactor want another one to run the tests. Ports go scatter the same way. If six repos all want :3000, na time to stop assigning ports by hand and make a Traefik reverse proxy route multiple apps under Docker Compose dispatch dem by hostname.
More people. tmux sockets dey per-user. So, two developers for the same box go each get their own tmux server, and dem no fit see each other's sessions. If una share one session through shared socket, everybody go dey type inside the same shell as the same Unix user. This get audit and permission consequences. Separate users na the boring but correct answer.
Unattended work. tmux na for interactive sessions wey you dey attach to. Jobs wey dey run on schedule without anybody watching belong inside a systemd unit and timer. Dem go get logging, restart policy, and survival across reboot by default. If you dey use tmux to run cron-shaped job, e mean say the job suppose be a service.
One last note: bind dev servers wey the agent start to 127.0.0.1, no be 0.0.0.0, and reach dem through an SSH tunnel (ssh -L 3000:127.0.0.1:3000 agent@your-server) instead of opening ports for ufw. Once you dey forward up to six ports, or phone and laptop both want access to the same preview, put a self-hosted WireGuard VPN on the VPS in front of dem instead. The dev servers go bind to a private interface, while ufw go continue deny everything from the public one. Firewall only fit help if you stop punching holes inside am.
Claude Code no be the only choice: running a coding AI agent on a VPS also consider Aider and Goose.
FAQ
Claude Code go continue to run after my SSH connection drop?
Only if you start am inside tmux. Process wey you launch directly from SSH shell na child of that shell, and e go die together with the pty when connection drop. Inside tmux, shell belong to detached tmux server, so agent go continue the work for where e dey, and tmux attach go put you back inside the same scrollback. Make tmux new -A -s <project> be the first command after every login, and the problem go stop.
I suppose install the CLI with sudo npm install -g?
No. Global prefix wey root own go give you EACCES errors for later installs, and files wey root own inside npm cache. Set npm prefix to ~/.npm-global (or use version manager like nvm), install as the unprivileged agent user, then export ~/.npm-global/bin to PATH from ~/.bashrc, above the interactive guard. If you don already run sudo npm once, repair the cache with sudo chown -R $(id -u):$(id -g) ~/.npm.
ssh -A agent forwarding safe for machine wey dey run agent?
E dey grant far more access than the job need. Forwarding exposes your local SSH agent socket to every process wey dey run as that user. So anything for the machine fit ask your key to sign for any host wey e fit reach, as long as you remain connected. Generate an ed25519 key for the server and register am as deploy key for each repository. Give write access only if agent really need push.
Why my build just dey print Killed?
One word without stack trace usually mean 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. Apply the fixes one after another: add swapfile, limit test and compiler parallelism, increase NODE_OPTIONS=--max-old-space-size=..., then move to bigger VPS. Note say OOM killer fit choose tmux server instead of the build, and this fit carry your whole session along.
tmux or systemd service?
tmux good for interactive sessions wey you attach to, monitor, and type into. Na exactly how agent session dey work. Work wey dey run on schedule without anybody watching suppose dey inside systemd unit and timer, where logging, restart policy, and survival after boot dey available by default. If you dey use tmux to run cron-shaped job, the job suppose be service.