Skip to content
·Build Logs·9 min read

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
  1. The architecture at a glance
  2. Starting state: no sudo, no tmux, no Claude
  3. Compiling tmux without root (four attempts)
  4. Dual GitHub identities that route themselves
  5. Claude Code on the VPS: OAuth + native migration
  6. Bonus: driving SPanel’s terminal with one JS call
  7. A day in the workstation
  8. What I skipped, and why
  9. 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.

Architecture: Linux laptop, MacBook, and Android phone all SSH into a single VPS over port 6543. Inside the VPS, a tmux session called main holds a persistent Claude Code process that git-pushes to two GitHub accounts.
Every device connects to one tmux session. Claude Code runs on the server, not the client.

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.

Four attempts to build tmux: first configure fails on libevent, second after building libevent fails on yacc, third after building bison configure succeeds, fourth runs make install, producing tmux 3.5a in home/.local/bin.
Two rounds of “configure fails, build the missing dependency, try again.”

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.

Four-step identity routing chain: directory match triggers includeIf, which loads .gitconfig-personal with URL rewrites, SSH resolves the github-personal host using the corresponding key, GitHub authenticates as the personal account.
Four config layers cooperate so I never switch accounts manually.

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.

Kiko Hernández Sáez

Kiko Hernández Sáez

Senior SEO & AI Growth Specialist with 10+ years building digital businesses. I write about the tools, systems, and workflows I use every day, from AI automation to terminal configs.

Share this article:

Need help building AI systems that scale?

I build custom AI agents, automation pipelines, and SEO systems for businesses.

View My Services