Advanced Claude Code VPS Setup: The Full Workstation Pattern
Turn your VPS into a persistent cloud workstation. Build tmux from source without sudo, run Claude Code remotely, and route dual GitHub identities.
In Claude Code on a VPS: Deploy and Build from Your Terminal I used the VPS as a remote hand: code lived on my laptop, and the server just ran the deploy. This post flips that. The VPS becomes my primary dev machine, and every device I own (a Linux laptop, a MacBook, an Android phone) becomes a disposable terminal that connects to it.
The catch: this VPS has no sudo. It is a Rocky Linux 9.7 shared-hosting account, with no tmux, no gh CLI, and no Claude Code preinstalled. Here is how I turned it into the dev environment that follows me everywhere, built with the same account-level tools you already have.
TL;DR
Compile tmux, libevent, and bison into ~/.local (no root needed). Install Claude Code via npm, OAuth login, then migrate to the native installer. Route two GitHub accounts automatically with includeIf + URL rewriting. SSH in from any device, tmux attach, and pick up exactly where you left off.
In This Guide click to collapse
- The architecture at a glance
- Starting state: no sudo, no tmux, no Claude
- Compiling tmux without root (four attempts)
- Dual GitHub identities that route themselves
- Claude Code on the VPS: OAuth + native migration
- Bonus: driving SPanel’s terminal with one JS call
- A day in the workstation
- What I skipped, and why
- FAQ
The architecture at a glance
Hub-and-spoke. One VPS is the hub; every device is a dumb terminal. Long-running sessions live inside tmux on the VPS, so disconnecting a device never kills work.
The core loop across any device:
ssh vps # my SSH alias
t # alias for: tmux attach -t main || tmux new -s main
claude # already authed, already in the right repo
Detach with Ctrl-a d, close the laptop, open the MacBook, run the same three commands: the session is exactly where I left it.
Starting state: no sudo, no tmux, no Claude
The target is a managed SPanel account on ScalaHosting. What the VPS had out of the box:
Starting inventory
| Category | Present | Missing |
|---|---|---|
| Runtime | Node 20, Python 3, git 2.47 | tmux, gh CLI, Claude Code |
| Build tools | gcc 11, g++, make, autoconf, pkg-config, perl | bison, yacc, libevent headers |
| Access | SSH on port 6543, SPanel web terminal | sudo, Docker, Tailscale, fail2ban control |
| Resources | 1.7 GB RAM, 18 GB free on /home | — |
No sudo is the whole challenge
Everything installs to ~/bin, ~/.local, and ~/.npm-global. No dnf install, no systemd services, no touching /etc. If you have gcc and make, almost every dev tool is reachable from userland.
First move: build the directory layout and extend PATH. Nothing fancy, but it lets everything else slot in without surprises.
mkdir -p ~/bin ~/src ~/personal ~/programming \\
~/.npm-global ~/.local/bin ~/.config
# In ~/.bashrc (managed block)
export PATH="\$HOME/bin:\$HOME/.local/bin:\$HOME/.npm-global/bin:\$PATH"
export NPM_CONFIG_PREFIX="\$HOME/.npm-global"
alias t='tmux attach -t main || tmux new -s main'
Compiling tmux without root (four attempts)
tmux needs two things the stock Rocky image does not have: libevent (networking) and yacc (parser generator). Without sudo, both compile to ~/.local.
Step 1 is libevent, built statically so it gets baked into the final tmux binary and has no runtime dependency:
cd ~/src
curl -sL https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz -o libevent.tar.gz
tar xzf libevent.tar.gz && cd libevent-2.1.12-stable
./configure --prefix="\$HOME/.local" --disable-shared --enable-static --disable-openssl
make -j2 && make install
Step 2 is bison (which provides yacc). tmux’s configure script hunts for yacc to parse its config grammar. Most distros pull bison in via tmux’s package deps, but building from source exposes every transitive dependency directly:
cd ~/src
curl -sL https://ftp.gnu.org/gnu/bison/bison-3.8.2.tar.gz -o bison.tar.gz
tar xzf bison.tar.gz && cd bison-3.8.2
./configure --prefix="\$HOME/.local" && make -j2 && make install
Step 3 is tmux itself, pointing configure at the headers and libs we just installed:
cd ~/src/tmux-3.5a
PATH="\$HOME/.local/bin:\$PATH" \\
PKG_CONFIG_PATH="\$HOME/.local/lib/pkgconfig" \\
CPPFLAGS="-I\$HOME/.local/include" \\
LDFLAGS="-L\$HOME/.local/lib" \\
./configure --prefix="\$HOME/.local"
make -j2 && make install
ln -sf ~/.local/bin/tmux ~/bin/tmux
tmux -V # tmux 3.5a
Why static linking matters here
Static libevent means the final tmux binary has zero library-path gymnastics. No LD_LIBRARY_PATH shims in .bashrc. Move the binary, it still works.
Dual GitHub identities that route themselves
I commit to two GitHub accounts from the same VPS: a personal one (kikocreates-personal) and a work one (kikocreates-work). I never want to think about which identity is active. The rule: anything under ~/personal is personal, anything under ~/programming is work. Git and SSH do the routing for me.
Two ed25519 keys, one per GitHub account, registered with their respective accounts:
ssh-keygen -t ed25519 -N "" -f ~/.ssh/github_personal -C "personal@workstation-vps"
ssh-keygen -t ed25519 -N "" -f ~/.ssh/github_work -C "work@workstation-vps"
SSH config gives each key a dedicated hostname:
# ~/.ssh/config
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/github_personal
IdentitiesOnly yes
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/github_work
IdentitiesOnly yes
Git rewrites URLs based on the current directory via conditional includes:
# ~/.gitconfig
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
[includeIf "gitdir:~/programming/"]
path = ~/.gitconfig-work
# ~/.gitconfig-personal
[user]
name = kikocreates-personal
email = kikocreates@personal.example
[url "git@github-personal:"]
insteadOf = git@github.com:
[url "git@github-personal:"]
insteadOf = https://github.com/
The flow on every command: working directory matches ~/personal/, git loads the personal config, the URL rewrite kicks in, SSH resolves github-personal to the right key, GitHub sees kikocreates-personal. Commits automatically use the right name and email. Zero thinking.
Gotcha: includeIf only fires inside a real git repo
includeIf "gitdir:~/personal/" matches only when there is a .git/ directory under that path. Running git config user.name in an empty folder returns your global default. Test by running git init first.
Claude Code on the VPS: OAuth + native migration
Claude Code ships a native installer, but it is not standalone: you bootstrap with npm, then run claude install to pull the native binary. After that you can uninstall the npm version.
# Bootstrap via npm (user-level, no sudo)
export NPM_CONFIG_PREFIX="\$HOME/.npm-global"
npm install -g @anthropic-ai/claude-code
# First run: OAuth flow
claude
# Claude prints a URL, I open it in my browser,
# GitHub/Claude.ai handles the OAuth dance.
# Auth state persists in ~/.claude/ on the VPS.
# Migrate to native installer
~/.npm-global/bin/claude install
# Installs to ~/.local/bin/claude and symlinks
# to ~/.local/share/claude/versions/<ver>/
# Clean up npm version
npm uninstall -g @anthropic-ai/claude-code
hash -r
which claude # ~/.local/bin/claude
The OAuth state is the key detail. Because the credentials live in ~/.claude/ on the server, every SSH session from every device inherits the same authenticated Claude. Log in once, work from anywhere forever.
Bonus: driving SPanel’s terminal with one JavaScript call
I did this whole setup through SPanel’s browser SSH terminal (an xterm.js canvas inside an iframe), driven via Playwright MCP. DOM-scraping a canvas is impossible, and clicking the helper textarea kept failing.
The breakthrough was finding wssh.send() on the iframe’s window object. It writes raw bytes directly to the WebSocket:
const iframe = document.querySelector('iframe');
iframe.contentWindow.wssh.send('tmux new -s main\\n');
This bypasses all DOM and keyboard-event complexity. It works on any webssh-based terminal (SPanel, Shellinabox, Wetty, most hosting panels). First place to check when you need to automate one: the iframe’s window for globals named wssh, term, wetty, or terminal.
A day in the workstation
From any device, the daily loop is three commands:
ssh vps
t # tmux attach or create "main"
claude # already authed, already in cwd
Detach with Ctrl-a d. The session stays alive on the server. New device, new network, same three commands, same exact state: same open files, same shell history, same Claude conversation.
Adding a new device takes 60 seconds
Generate an SSH key on the device, paste the public key into ~/.ssh/authorized_keys on the VPS (via any already-connected device), and the new device joins the cluster.
What I skipped, and why
Intentional omissions
| Tool | Reason | Revisit when |
|---|---|---|
| Tailscale / WireGuard | Needs root to install | Admin account or different host |
| mosh | Needs root for mosh-server | Unreliable mobile networks |
| Docker | No root. Use SPanel’s managed services instead. | Moving off shared hosting |
| tmux-continuum / tmux-resurrect | Adds a plugin manager for modest benefit | Frequent VPS reboots destroy sessions |
| SSH agent forwarding | Keys-on-VPS is simpler, fewer moving parts | Key-leak blast radius matters |
The point of these omissions: most “cloud workstation” tutorials assume root. This one does not. Everything here runs inside an account-level home directory, which means the exact same pattern works on any shared-hosting box that gives you SSH, gcc, and make.
FAQ
Can I do this without sudo on any shared hosting?
If the host gives you SSH access plus gcc, make, and roughly 500 MB of free space in your home directory, yes. You need gcc to compile libevent, bison, and tmux. You need about 300 MB for the build artifacts in ~/src and roughly 150 MB for the installed binaries in ~/.local. Node.js is the only system dependency you cannot easily compile yourself in reasonable time, so the host has to provide Node 18+.
Why tmux instead of zellij, screen, or byobu?
tmux is the most widely documented and has the fewest surprises on minimal RHEL-family boxes. Zellij is more modern but adds a Rust toolchain dependency for some setups. Screen is present on most systems but has awkward session detachment behavior. Byobu is a wrapper around tmux anyway. On a constrained environment where I cannot debug easily, I want the tool with the most StackOverflow answers.
What happens if the VPS reboots?
tmux sessions die on reboot. This is the main weakness of the pattern. Options to mitigate: use tmux-resurrect (a plugin that saves and restores layouts, though it does not restore running processes), set up a systemd user service to auto-start a default tmux session at boot (only works if your host exposes systemctl --user), or just accept that after a reboot you run tmux new -s main once and continue. For most shared-hosting VPS instances, reboots happen every few months and the pattern is fine.
Can this coexist with the deploy workflow from Part 1?
Yes. The deploy target VPS from the previous post and the workstation VPS can be the same machine. Your ~/deploy.sh still works exactly the same way. The workstation pattern just adds tmux, Claude Code, and dual git identities on top of what you already have. If you are already paying for a VPS to host your site, this doubles its usefulness without adding infrastructure.
How is this different from VS Code Remote SSH or similar?
VS Code Remote SSH still keeps your IDE state on the client: which files are open, the terminal history, editor panels. If you switch machines, you reopen every file. The tmux-on-VPS pattern keeps every piece of session state on the server. Your editor (running in the tmux pane) stays at the same cursor position. Your shell history is continuous. Your Claude conversation persists. Different tool for a different problem: VS Code Remote is great for polished IDE workflows, tmux is great for continuity across any terminal on any device.
Do I need the Playwright MCP to replay this?
No. I used Playwright because I wanted the whole build to happen in SPanel’s browser terminal for auditability. If you have direct SSH to the VPS (which you will, after the first key is uploaded), just open a terminal and paste commands. Playwright was a convenience, not a requirement.
How do I keep my dotfiles synced to the VPS?
Put them in a git repository (a dotfiles repo under ~/personal/ fits the pattern nicely). The repo’s install.sh symlinks .bashrc, .tmux.conf, .gitconfig, etc. into place. Whenever I change a config on the VPS, I commit and push. Every other device just runs git pull. The VPS becomes both the workstation and the source of truth for configuration.
