Skip to content

Claude Code VPS Setup

Created 2026-03-02
Updated 2026-03-03
Status active
Tags agent-systemclaude-codevpssetupmulti-user

Status: Complete — All core phases done (2026-03-03) Replaces: OpenClaw and CrewAI (both decommissioned as part of this setup) Architecture: Claude Code as the intelligence layer, n8n as a lightweight automation/routing layer No API keys or OpenRouter: All intelligence tasks run through Patrick’s Claude Max account and Asia’s Claude Pro account. No OpenRouter, no Anthropic API billing.


This document covers setting up Claude Code on the OpenClaw VPS for both Patrick and Asia. Each user gets their own Linux account, their own Claude Code session authenticated with their personal Claude.ai subscription (Max/Pro), and access to shared Baseworks project directories. n8n on the separate Agents VPS connects to Claude Code via SSH to run headless tasks and post results to Slack.

This replaces the CrewAI + OpenClaw stack entirely. CrewAI was the previous “brain” layer and used OpenRouter for API access. Claude Code fills that role directly using each user’s personal Claude.ai subscription — no API keys, no OpenRouter, no per-token billing.

n8n remains on the Agents VPS but in a reduced, non-AI role: Slack event routing, scheduled git syncs, and simple triggers. It no longer makes AI calls. Any task requiring intelligence is handled by Claude Code directly, either interactively in a tmux session or headlessly via claude -p.


ServerIPSpecRole
Claude Code VPS46.224.129.16CX33 — 4 vCPU, 8GB RAM, 75GBClaude Code (Patrick + Asia)
Agents VPS167.235.236.99CX33 — 4 vCPU, 8GB RAM, 80GBn8n + postgres

SSH access:

Terminal window
ssh patrick@46.224.129.16 # Claude Code VPS
ssh patrick@167.235.236.99 # Agents VPS (n8n)

Slack (message from Patrick or Asia)
|
v
n8n Slack Event Router (Agents VPS — 167.235.236.99)
|
v
n8n SSH Execute node
| ssh patrick@46.224.129.16 'claude -p "..." --output-format json'
v
Claude Code VPS (46.224.129.16)
|
v
Result (JSON) returned to n8n
|
v
n8n posts result to Slack channel

Claude Code also runs interactively — Patrick and Asia can SSH in directly and work in their own tmux sessions, accessing the shared project directories and Git repositories.


Phase 1: Stop OpenClaw (preserve config intact)

Section titled “Phase 1: Stop OpenClaw (preserve config intact)”

OpenClaw stays on this server but in a stopped state. The container, config, volumes, NGINX proxy, and Cloudflare DNS record are all preserved — nothing is removed. A stopped container has zero attack surface. If OpenClaw is ever needed for evaluation, it can be restarted with a single command.

This is the right posture for personal/dev use: Claude Code runs natively as a host process, OpenClaw stays sandboxed in Docker but offline.

Terminal window
ssh patrick@46.224.129.16
# Stop OpenClaw (leaves config, volumes, and images intact)
cd /opt/baseworks-claw/openclaw
sudo docker compose stop
# Verify it is stopped
sudo docker ps
# openclaw-openclaw-gateway-1 should not appear in the running list
# Disable the NGINX site for claw.baseworks.com while OpenClaw is offline
# (avoids serving a broken proxy to the public)
sudo ln -sf /dev/null /etc/nginx/sites-enabled/claw.baseworks.com
sudo nginx -t && sudo systemctl reload nginx

To bring OpenClaw back — apply hardened config first, then start:

Step 1: Update docker-compose.override.yml with full hardening

Terminal window
sudo nano /opt/baseworks-claw/openclaw/docker-compose.override.yml

Replace the contents with:

services:
openclaw-gateway:
user: "1000:1000"
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
networks:
- openclaw-net
environment:
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
networks:
openclaw-net:
driver: bridge

What each setting does:

  • user: "1000:1000" — runs as non-root (already the case, now explicit)

  • read_only: true — container filesystem is read-only; only the mounted volumes (config, workspace, state, credentials) remain writable

  • tmpfs: /tmp — gives Node.js a writable temp directory in RAM, since the main filesystem is read-only

  • cap_drop: [ALL] — drops all Linux kernel capabilities; OpenClaw is a Node.js app on port 18789 and needs none

  • no-new-privileges: true — prevents any process inside the container from gaining elevated privileges

  • openclaw-net — dedicated Docker bridge network, isolating OpenClaw from any other Docker services on the host

Note on read_only: true: If OpenClaw fails to start with this enabled (some Node.js apps write to unexpected internal paths), remove read_only: true and the tmpfs line and try again. The remaining hardening (cap_drop, no-new-privileges, dedicated network, non-root user) is still a significant improvement over the default config.

Step 2: Start and re-enable NGINX

Terminal window
cd /opt/baseworks-claw/openclaw
sudo docker compose up -d openclaw-gateway
sudo docker ps # confirm container is running
sudo ln -sf /etc/nginx/sites-available/claw.baseworks.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

The Cloudflare DNS record (claw.baseworks.com) stays pointing at this server throughout, so it comes back online immediately with no DNS changes needed.

Migration note: If OpenClaw ever needs to move to its own VPS, everything under /opt/baseworks-claw/ plus the NGINX config and SSL certs (/etc/ssl/cloudflare/) can be rsynced to a new server in under an hour. See OpenClaw-Deployment-Guide for full reference.

Docker itself stays installed — not needed for Claude Code but causes no harm.


Patrick’s account already exists. Only Asia needs to be added.

Terminal window
ssh patrick@46.224.129.16
# Create Asia's account
sudo adduser asia
sudo usermod -aG sudo asia
# Create shared baseworks group
sudo groupadd baseworks
sudo usermod -aG baseworks patrick
sudo usermod -aG baseworks asia
# Set up Asia's SSH access (add her public key)
sudo mkdir -p /home/asia/.ssh
sudo nano /home/asia/.ssh/authorized_keys
# Paste Asia's SSH public key, save and exit
sudo chown -R asia:asia /home/asia/.ssh
sudo chmod 700 /home/asia/.ssh
sudo chmod 600 /home/asia/.ssh/authorized_keys

Terminal window
# Create shared directory structure
sudo mkdir -p /srv/baseworks/{website,knowledge-base,automation,shared-config}
sudo chown -R root:baseworks /srv/baseworks
sudo chmod -R 2775 /srv/baseworks
# Clone shared Git repos into the shared directory
cd /srv/baseworks
# Knowledge base vault (read/write for both users)
sudo git clone https://github.com/p-oancia/baseworks-kb-shared-brain.git knowledge-base
sudo chown -R root:baseworks knowledge-base
sudo chmod -R 2775 knowledge-base
# Changelog repo
sudo git clone https://github.com/p-oancia/baseworks-changelog.git changelog
sudo chown -R root:baseworks changelog
sudo chmod -R 2775 changelog

Configure Git identity for the shared directory:

Terminal window
cd /srv/baseworks/knowledge-base
sudo git config user.name "Baseworks Agent"
sudo git config user.email "agents@baseworks.com"
cd /srv/baseworks/changelog
sudo git config user.name "Baseworks Agent"
sudo git config user.email "agents@baseworks.com"

Note: The GitHub PAT from the Agents VPS (expires ~2026-05-19) can be reused here, or a new fine-grained PAT created specifically for this VPS. Store it in the remote URL: https://<token>@github.com/p-oancia/repo.git


Install Claude Code for each user separately. Each installation is independent and lives in that user’s home directory.

As patrick:

Terminal window
# SSH in as patrick (or sudo su - patrick)
curl -fsSL https://claude.ai/install.sh | bash
# Verify
claude --version

As asia:

Terminal window
# SSH in as asia (or sudo su - asia)
curl -fsSL https://claude.ai/install.sh | bash
# Verify
claude --version

Claude Code installs to ~/.local/bin/claude and config lives in ~/.claude/. Each user’s credentials and settings are fully isolated.


Authentication requires a browser on first login. On a headless VPS, this is handled by copying the login URL.

For each user, on their first claude launch:

Terminal window
claude
# Claude Code starts and says: "Open this URL in your browser to log in"
# If the URL doesn't open automatically, press C to copy it
# Paste the URL into a browser on your local Mac
# Log in with your Claude.ai account (Patrick → Max, Asia → Pro or Max)
# Credentials are saved on the VPS — you will not need to do this again

After completing the browser login, credentials are stored in ~/.claude/ and persist across sessions and reboots.


tmux keeps sessions alive when you disconnect from SSH. Both users work in their own named sessions.

Terminal window
# Patrick's sessions
tmux new -s patrick-kb # knowledge base work
tmux new -s patrick-site # website work
tmux new -s patrick-agents # agent tasks
# Asia's sessions (as asia user)
tmux new -s asia-kb
tmux new -s asia-agents
# Detach from a session: Ctrl+B then D
# List sessions: tmux ls
# Reattach: tmux attach -t patrick-kb

Start Claude Code in a session:

Terminal window
tmux attach -t patrick-kb
cd /srv/baseworks/knowledge-base
claude

Add MCP servers for each user. These commands are run inside each user’s shell (not as root).

GitHub MCP (HTTP, OAuth — authenticates via browser on first use):

Terminal window
claude mcp add --transport http github https://api.githubcopilot.com/mcp/
# Then inside Claude Code: /mcp → Authenticate → GitHub

Filesystem MCP (gives Claude Code access to shared project files):

Terminal window
claude mcp add --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /srv/baseworks

Verify inside Claude Code:

/mcp

Both users run these commands separately — MCP configs are per-user by default.


Create a shared project config that Claude Code reads automatically when working in the /srv/baseworks directory.

Terminal window
sudo nano /srv/baseworks/CLAUDE.md

Contents:

# Baseworks — Claude Code Project Config
## Organization
International movement methodology company (Montreal, Tokyo, Europe).
Team: Patrick (founder, technical lead) and Asia/Ksenia (co-founder, neuroscientist).
## Technology Stack
- WordPress + Bricks Builder (baseworks.com)
- xCloud (server management)
- Obsidian + Git (knowledge base)
- n8n (automation — separate VPS at 167.235.236.99)
- Claude Code (this environment)
- Slack (communication layer — baseworks.slack.com)
## Terminology (non-negotiable)
- Never use "pigeon" — correct term is "Z Expansion C-Tuck"
- Use "Forms" not "exercises"
- Capitalize Movement Principles and Focus types
- "Physical Intelligence" is b-roll — not a hero or heading term
- Practice Platform is gated — not open enrollment
- WHILE-NOT-IF-DO is the algorithmic framework name
## Git Repositories
- baseworks-kb-shared-brain → /srv/baseworks/knowledge-base
- baseworks-changelog → /srv/baseworks/changelog
## Slack Channels
- #wordpress-updates (C0AFWBZDDR9)
- #forum-responses (C0AG6BZEA2Y)
- #content-strategy (C0AG0MUFE1L)
- #agent-alerts (C0AGFM6AZ09)
- #vault-inbox (C0AG0MWBP5L)
## Current Priorities
1. Elementor → Bricks Builder migration on baseworks.com
2. Knowledge base development
3. Multi-agent workflow refinement
4. Montreal programs through June 2026
## Instructions
- Preserve all repo names, Slack channel names, and terminology above
- Always git pull before making changes to shared repos
- Commit with author "Baseworks Agent" <agents@baseworks.com> for automated tasks
- Read /srv/baseworks/changelog/CLAUDE-INSTRUCTIONS.md before any site-related work

Phase 9: n8n Bridge (Slack → Claude Code)

Section titled “Phase 9: n8n Bridge (Slack → Claude Code)”

This is what replaces CrewAI in the existing n8n workflows. Instead of n8n calling the CrewAI container at crewai:8000, it SSHes into the Claude Code VPS and runs claude -p.

n8n needs to SSH into the Claude Code VPS. Add the n8n VPS’s public key to the Claude Code VPS:

Terminal window
# On the Agents VPS (167.235.236.99) — get the SSH public key
cat ~/.ssh/id_rsa.pub
# If none exists: ssh-keygen -t ed25519 -C "n8n-agents-vps"
# On the Claude Code VPS (46.224.129.16) — authorize it
echo "ssh-ed25519 ..." >> /home/patrick/.ssh/authorized_keys

In any n8n workflow that previously called CrewAI, replace the HTTP Request node with an SSH Execute node:

Connection settings:

  • Host: 46.224.129.16
  • Port: 22
  • Username: patrick
  • Auth: SSH key (paste the private key from the Agents VPS)

Command:

Terminal window
cd /srv/baseworks && claude -p "{{$json.task}}" --output-format json --max-turns 10

claude -p runs use Patrick’s authenticated Claude Max session — no API key or OpenRouter involved. Usage counts against the Max subscription, same as interactive use.

Parse the result with a Code node:

const output = JSON.parse($input.first().json.stdout);
return [{ json: { result: output.result, session_id: output.session_id } }];

The previous CrewAI-dependent workflows are archived. n8n’s role is now limited to orchestration and routing — it does not make AI calls.

WorkflowStatusAction
Vault Git SyncKeep — no AI involvedReactivate as-is
Kill SwitchKeep — no AI involvedReactivate as-is
Slack Event RouterKeep — routing onlyReactivate, update to route to Claude Code tasks
Vault Capture via SlackSimplifyn8n receives Slack message → SSH Execute → claude -p → post result
Daily Vault SummaryDeferRun on demand in Claude Code for now; automate later if needed
Content Creation PipelineArchiveHandle directly in Claude Code sessions
Forum Response PipelineArchiveHandle directly in Claude Code sessions
WordPress MonitoringPendingStill needs WP_URL + HTTP Basic Auth — separate task

Reactivate in order on the Agents VPS. All workflows are currently INACTIVE (intentional pause from 2026-02-25).

1. Vault Git Sync — no AI, activate immediately
2. Kill Switch — no AI, activate immediately
3. Slack Event Router — no AI, activate immediately
4. Vault Capture — update to use SSH Execute first, then activate
5. Daily Vault Summary — update to use SSH Execute first, then activate
6. Content Creation — update to use SSH Execute first, then activate
7. WordPress Monitoring — still needs WP_URL + HTTP Basic Auth (separate task)
8. Forum Response — still needs FORUM_API_URL (separate task)

Once all workflows using AI have been migrated to Claude Code and tested:

Terminal window
ssh patrick@167.235.236.99
cd /opt/baseworks-vault/03-resources/agent-system/docker
sudo docker stop baseworks-crewai
sudo docker rm baseworks-crewai
# Remove from docker-compose.yml (edit to remove the crewai service block)
sudo nano docker-compose.yml
# Remove the crewai image
sudo docker image rm baseworks-crewai

The postgres and n8n containers remain untouched.


Infrastructure Control via Slack or Discord

Section titled “Infrastructure Control via Slack or Discord”

All server administration — container management, config edits, nginx, firewall rules — can be issued as natural language messages in Slack or Discord. The pipeline is n8n as the routing layer, Claude Code on the VPS as the executor.

"Stop OpenClaw and disable its nginx proxy" (Slack #agent-alerts or Discord)
n8n Slack/Discord Event Router
SSH Execute → claude -p "..." --allowedTools "Bash(sudo docker *),
Bash(sudo systemctl *),Bash(sudo ufw *),Edit"
Claude Code runs the appropriate sudo commands with full VPS authority
Confirmation posted back to Slack/Discord

Allowed tool scopes for infrastructure tasks

Section titled “Allowed tool scopes for infrastructure tasks”

Rather than --dangerously-skip-permissions (which approves everything blindly), use --allowedTools to pre-authorise specific command categories per task type:

Task category--allowedTools value
Container managementBash(sudo docker *)
NGINX / servicesBash(sudo systemctl *)
FirewallBash(sudo ufw *)
Config file editsEdit
Full infra controlBash(sudo docker *),Bash(sudo systemctl *),Bash(sudo ufw *),Edit

Example n8n SSH Execute command for infra tasks

Section titled “Example n8n SSH Execute command for infra tasks”
Terminal window
cd /srv/baseworks && claude -p "{{$json.task}}" \
--allowedTools "Bash(sudo docker *),Bash(sudo systemctl *),Bash(sudo ufw *),Edit" \
--output-format json \
--max-turns 5

Example Slack commands that translate naturally

Section titled “Example Slack commands that translate naturally”
  • “Stop OpenClaw” → sudo docker compose stop in /opt/baseworks-claw/openclaw
  • “Restart OpenClaw with hardened config” → updates override file, runs docker compose up -d
  • “Disable OpenClaw nginx” → disables site in /etc/nginx/sites-enabled/, reloads nginx
  • “Show firewall rules” → sudo ufw status verbose
  • “Block port 8080” → sudo ufw deny 8080
  • “Show running containers” → sudo docker ps
  • “Exec into OpenClaw and show logs” → sudo docker logs openclaw-openclaw-gateway-1

Patrick’s account has passwordless sudo — Claude Code executes all of these with full authority. Asia’s account can be granted the same if needed.

Discord note: Discord works identically to Slack via n8n. n8n has a native Discord trigger node. Switch the Event Router workflow input from Slack to Discord (or run both in parallel) to support either interface.


Remote Control connects claude.ai/code and the Claude mobile app (iOS/Android) to a Claude Code session running on the VPS. Start a session at your desk, then continue it from your phone, tablet, or any browser — full interactive conversation, not one-shot commands.

Available on: Max plan now. Pro plan coming soon. Docs: https://code.claude.com/docs/en/remote-control

tmux solves the only real limitation (the terminal process must stay running). On the VPS, the process keeps running inside tmux even after you close your SSH connection or shut your laptop.

Terminal window
ssh patrick@46.224.129.16
tmux attach -t patrick-kb
# Start Remote Control — generates a URL and QR code
claude remote-control
# Press spacebar to toggle the QR code display
# Scan from phone or open the URL in any browser
# Then detach from tmux: Ctrl+B D
# Close your laptop — session stays alive on the VPS
# Open on phone — full interactive conversation continues

Inside Claude Code run:

/config

Set Enable Remote Control for all sessions to true — every Claude Code session will have Remote Control active without needing to run the command manually.

Each tmux session is an independent Claude Code instance with its own Remote Control URL:

  • patrick-kb → URL for knowledge base work
  • patrick-site → URL for website work
  • asia-kb → Asia’s own independent session

Each is accessible separately from any device.

/rename "KB — vault filing"

Named sessions appear in the session list on claude.ai/code and in the Claude app with a green status dot when online.

Remote Control uses outbound HTTPS only — no inbound ports open on the VPS. All traffic goes through the Anthropic API over TLS with short-lived, purpose-scoped credentials.


Terminal window
ssh patrick@46.224.129.16
tmux attach -t patrick-kb # or whichever session
claude

Run a headless task (from n8n or terminal)

Section titled “Run a headless task (from n8n or terminal)”
Terminal window
claude -p "Your task here" --output-format json --max-turns 10
Terminal window
curl -fsSL https://claude.ai/install.sh | bash
# Native installs auto-update in the background — manual update rarely needed

Check MCP server status (inside Claude Code)

Section titled “Check MCP server status (inside Claude Code)”
/mcp

  • UFW on the Claude Code VPS already restricts to ports 22, 80, 443 (set up during OpenClaw deployment)
  • Fail2ban is active for SSH brute-force protection
  • Claude Code does not expose any network ports — it runs as a local process only
  • n8n SSH access uses key-based auth only (no password)
  • Each user’s Claude credentials are stored in their own ~/.claude/ — fully isolated