Context
Notes from a real Hetzner VPS deployment (Ubuntu 24.04) to inform official documentation. These are key lessons and steps for installing QuadWork on a remote VPS.
Critical: Do NOT Run as Root
Claude Code blocks --dangerously-skip-permissions when running as root/sudo. This means agents will crash immediately if QuadWork is running under the root user.
Solution: Create a dedicated non-root user before installing anything.
useradd -m -s /bin/bash quadwork
mkdir -p /projects
chown quadwork:quadwork /projects
Grant passwordless sudo (needed for nginx, certbot, system packages):
echo "quadwork ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/quadwork
chmod 440 /etc/sudoers.d/quadwork
Copy SSH keys so you can connect directly as the new user:
mkdir -p /home/quadwork/.ssh
cp /root/.ssh/authorized_keys /home/quadwork/.ssh/authorized_keys
chown -R quadwork:quadwork /home/quadwork/.ssh
chmod 700 /home/quadwork/.ssh
chmod 600 /home/quadwork/.ssh/authorized_keys
Then update your local ~/.ssh/config:
Host my-vps
User quadwork
HostName <server-ip>
Port 22
IdentityFile ~/.ssh/<your-key>
All subsequent steps should be run as the quadwork user, not root.
Prerequisites
Node.js via nvm (REQUIRED — do NOT use system Node)
# As the quadwork user
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm install 24
nvm use 24
Do NOT install Node via apt or nodesource. Only use nvm. Using system Node alongside nvm creates PATH conflicts — pm2 and QuadWork will spawn agents with the system PATH (which doesn't include nvm binaries), causing Claude Code and Codex to either not be found or lose access to their auth credentials. This results in agents getting stuck on login/onboarding prompts even though the CLI works fine in interactive SSH sessions.
nvm also avoids permission issues with global npm installs and lets you pin a specific Node version.
GitHub CLI
Install as root (system package), then authenticate as the quadwork user:
# As root
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main' > /etc/apt/sources.list.d/github-cli.list
apt-get update && apt-get install -y gh
Claude Code & Codex CLI
# As quadwork user, with nvm loaded
npm install -g @anthropic-ai/claude-code
npm install -g @openai/codex
All binaries (claude, codex, quadwork, pm2) will live under ~/.nvm/versions/node/v24.x.x/bin/. No symlinks needed — as long as pm2 is started with nvm's PATH (see pm2 section below).
Install & Configure QuadWork
# As quadwork user
npm install -g quadwork@latest
The config file lives at ~/.quadwork/config.json. You can pre-create it to set the dashboard port before running init:
{
"port": 3000,
"agentchattr_url": "http://127.0.0.1:8300",
"agentchattr_dir": "",
"projects": []
}
Then run the interactive setup:
Process Management with pm2
npm install -g pm2
# IMPORTANT: Start QuadWork with nvm loaded so the process inherits the full PATH.
# This ensures spawned agents (Claude, Codex) can find their binaries and auth.
# nvm must be sourced in the current shell before running these commands.
pm2 start $(which quadwork) --name quadwork --interpreter $(which node) -- start
pm2 save
# Auto-start on reboot — run the generated command as root.
# The systemd service captures the current PATH, so nvm's bin dir is preserved.
pm2 startup systemd
Important: Always run pm2 save while the process is in online state. pm2 saves a snapshot (dump) of process states — if you save while a process is stopped, it will resurrect as stopped on every reboot, even though the systemd service starts fine. If this happens, start the process and re-save:
pm2 start quadwork
pm2 save
Note: pm2 should be installed and run under the quadwork user, not root. If you previously had pm2 under root, clean it up:
# As root
pm2 kill
pm2 unstartup systemd
rm -rf /root/.pm2
Domain Setup with Nginx + SSL
DNS
Create an A record pointing your subdomain (e.g. app.example.com) to the server IP.
Nginx reverse proxy
# As root
apt-get install -y nginx
Create /etc/nginx/sites-available/app.example.com:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}
The proxy_read_timeout 86400 and WebSocket upgrade headers are important for the live terminal/agent PTY connections.
ln -sf /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
SSL with Let's Encrypt
apt-get install -y certbot python3-certbot-nginx
certbot --nginx -d app.example.com --non-interactive --agree-tos -m your@email.com
Certbot auto-configures nginx for HTTPS and sets up auto-renewal.
Basic HTTP Authentication (Recommended)
Since the dashboard is publicly accessible, add password protection:
# As root
apt-get install -y apache2-utils
htpasswd -c /etc/nginx/.htpasswd admin
Add to the nginx server block (inside the server { ... } with listen 443 ssl):
auth_basic "QuadWork";
auth_basic_user_file /etc/nginx/.htpasswd;
nginx -t && systemctl reload nginx
Store credentials in a .env file for reference (e.g. ~/.quadwork/.env) with chmod 600.
Known Issues on VPS
1. Claude Code root restriction
--dangerously-skip-permissions is blocked when running as root. Must use a non-root user.
2. Trust prompt blocks agents
New worktree directories trigger Claude Code's "Do you trust this directory?" prompt. Agents can't answer this interactively. Pre-trust directories with:
claude config set trust_directory /path/to/worktree
3. AgentChattr race condition
AgentChattr may not have bound its port before agents try to register. Related: #579, #580.
4. PATH issues — do NOT use symlinks
If you install Node via both apt and nvm, or start pm2 without nvm loaded, agent subprocesses will inherit the system PATH instead of nvm's. This causes Claude Code to lose access to its auth credentials and show login/onboarding prompts even when claude works fine interactively.
Do not fix this with symlinks (e.g. ln -s ~/.nvm/.../claude /usr/local/bin/claude). Symlinks resolve the binary but not the environment — Claude Code still won't find its credentials properly.
The correct fix: Only install Node via nvm, and always have nvm loaded before starting pm2. The pm2 systemd startup service will capture the PATH from the session where pm2 startup was run.
Quick Reference: Full Install Order
- Create non-root user + SSH keys
- Install nvm + Node.js 24 (as quadwork user)
- Install GitHub CLI (as root, auth as quadwork user)
- Install Claude Code + Codex CLI (as quadwork user via nvm)
- Install QuadWork + pm2 (as quadwork user via nvm)
- Configure
~/.quadwork/config.json with desired port
- Run
quadwork init for interactive setup
- Start with pm2 and configure startup
- Set up nginx reverse proxy + SSL
- Add HTTP basic auth
- Pre-trust worktree directories for Claude Code
This ticket is intended as raw notes for the documentation agent to turn into a polished install guide.
Context
Notes from a real Hetzner VPS deployment (Ubuntu 24.04) to inform official documentation. These are key lessons and steps for installing QuadWork on a remote VPS.
Critical: Do NOT Run as Root
Claude Code blocks
--dangerously-skip-permissionswhen running as root/sudo. This means agents will crash immediately if QuadWork is running under the root user.Solution: Create a dedicated non-root user before installing anything.
Grant passwordless sudo (needed for nginx, certbot, system packages):
Copy SSH keys so you can connect directly as the new user:
Then update your local
~/.ssh/config:All subsequent steps should be run as the
quadworkuser, not root.Prerequisites
Node.js via nvm (REQUIRED — do NOT use system Node)
Do NOT install Node via
aptornodesource. Only use nvm. Using system Node alongside nvm creates PATH conflicts — pm2 and QuadWork will spawn agents with the system PATH (which doesn't include nvm binaries), causing Claude Code and Codex to either not be found or lose access to their auth credentials. This results in agents getting stuck on login/onboarding prompts even though the CLI works fine in interactive SSH sessions.nvm also avoids permission issues with global npm installs and lets you pin a specific Node version.
GitHub CLI
Install as root (system package), then authenticate as the quadwork user:
Claude Code & Codex CLI
# As quadwork user, with nvm loaded npm install -g @anthropic-ai/claude-code npm install -g @openai/codexAll binaries (
claude,codex,quadwork,pm2) will live under~/.nvm/versions/node/v24.x.x/bin/. No symlinks needed — as long as pm2 is started with nvm's PATH (see pm2 section below).Install & Configure QuadWork
# As quadwork user npm install -g quadwork@latestThe config file lives at
~/.quadwork/config.json. You can pre-create it to set the dashboard port before runninginit:{ "port": 3000, "agentchattr_url": "http://127.0.0.1:8300", "agentchattr_dir": "", "projects": [] }Then run the interactive setup:
Process Management with pm2
Important: Always run
pm2 savewhile the process is in online state. pm2 saves a snapshot (dump) of process states — if you save while a process is stopped, it will resurrect as stopped on every reboot, even though the systemd service starts fine. If this happens, start the process and re-save:Note: pm2 should be installed and run under the
quadworkuser, not root. If you previously had pm2 under root, clean it up:Domain Setup with Nginx + SSL
DNS
Create an A record pointing your subdomain (e.g.
app.example.com) to the server IP.Nginx reverse proxy
# As root apt-get install -y nginxCreate
/etc/nginx/sites-available/app.example.com:The
proxy_read_timeout 86400and WebSocket upgrade headers are important for the live terminal/agent PTY connections.ln -sf /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/ nginx -t && systemctl reload nginxSSL with Let's Encrypt
Certbot auto-configures nginx for HTTPS and sets up auto-renewal.
Basic HTTP Authentication (Recommended)
Since the dashboard is publicly accessible, add password protection:
# As root apt-get install -y apache2-utils htpasswd -c /etc/nginx/.htpasswd adminAdd to the nginx server block (inside the
server { ... }withlisten 443 ssl):nginx -t && systemctl reload nginxStore credentials in a
.envfile for reference (e.g.~/.quadwork/.env) withchmod 600.Known Issues on VPS
1. Claude Code root restriction
--dangerously-skip-permissionsis blocked when running as root. Must use a non-root user.2. Trust prompt blocks agents
New worktree directories trigger Claude Code's "Do you trust this directory?" prompt. Agents can't answer this interactively. Pre-trust directories with:
claude config set trust_directory /path/to/worktree3. AgentChattr race condition
AgentChattr may not have bound its port before agents try to register. Related: #579, #580.
4. PATH issues — do NOT use symlinks
If you install Node via both apt and nvm, or start pm2 without nvm loaded, agent subprocesses will inherit the system PATH instead of nvm's. This causes Claude Code to lose access to its auth credentials and show login/onboarding prompts even when
claudeworks fine interactively.Do not fix this with symlinks (e.g.
ln -s ~/.nvm/.../claude /usr/local/bin/claude). Symlinks resolve the binary but not the environment — Claude Code still won't find its credentials properly.The correct fix: Only install Node via nvm, and always have nvm loaded before starting pm2. The pm2 systemd startup service will capture the PATH from the session where
pm2 startupwas run.Quick Reference: Full Install Order
~/.quadwork/config.jsonwith desired portquadwork initfor interactive setupThis ticket is intended as raw notes for the documentation agent to turn into a polished install guide.