Remote Dev Server Guide Part 4: Claude Code & Development Workflow
In Part 3, we locked down our server with Tailscale. Now let's set up the development workflow that makes this setup genuinely better than working locally.
Why Claude Code on a Remote Server?
Claude Code is Anthropic's CLI tool for AI-assisted development. It runs in your terminal, reads your codebase, edits files, runs commands, and helps you build features. Running it on a remote server has unique advantages:
- Persistent sessions — Claude Code keeps its context in tmux sessions that survive disconnects
- Powerful hardware — your 8-core server handles builds and AI processing faster than most laptops
- Work from anywhere — SSH in from your Mac, iPad, or phone and pick up where you left off
- No battery drain — heavy computation happens on the server, not your laptop
Install Claude Code
Claude Code offers a standalone installer that downloads a self-contained binary — no npm required:
curl -fsSL https://claude.ai/install.sh | sh
This installs the claude binary to ~/.local/bin/. Make sure it's in your PATH (the installer usually handles this). Then authenticate with your Anthropic account:
claude
# Follow the authentication prompts
Claude Code stores its configuration in ~/.claude/:
~/.claude/
├── CLAUDE.md # Global instructions for all projects
├── settings.json # Preferences
├── .credentials.json # Auth tokens
└── projects/ # Per-project memory and history
Configure CLAUDE.md
The ~/.claude/CLAUDE.md file gives Claude Code persistent context about your environment. This is crucial for a remote server because Claude needs to know things like "don't start dev servers in the background" and "use tmux for long-running processes."
Here's a trimmed version of what I use:
# System Context
## Environment
- **Host**: Hetzner dedicated server
- **OS**: Ubuntu 24.04 LTS
- **Working directory**: /home/dev
## Hardware
- **CPU**: AMD Ryzen 7 3700X — 8 cores / 16 threads
- **RAM**: 64 GB
## IMPORTANT: Dev Server Process Management
**NEVER launch dev servers using background processes.**
Background processes get detached and become zombies.
**Correct approach:**
- Always start dev servers inside tmux using `tmux send-keys`
- Example: `tmux send-keys -t dev "bun run dev" Enter`
## Projects
### Development (dev user, /home/dev/projects/)
- **my-app** — Next.js app. Dev port: 3000
### Production (deploy user, /var/www/)
- **my-app** — Production port: 3003
This prevents Claude Code from accidentally creating zombie processes (a real problem I hit early on) and gives it awareness of your project layout.
Configure settings.json
Claude Code's behavior is controlled through ~/.claude/settings.json. Here's my configuration:
{
"env": {
"MAX_THINKING_TOKENS": "63000",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "16384",
"BASH_DEFAULT_TIMEOUT_MS": "600000"
},
"permissions": {
"allow": [
"Bash(git:*)",
"Bash(bun:*)",
"Read(./**)",
"Edit(./**)"
],
"deny": [
"Read(./secrets/**)",
"Read(./**/credentials*)"
],
"defaultMode": "acceptEdits"
},
"alwaysThinkingEnabled": true,
"effortLevel": "high"
}
What each setting does:
MAX_THINKING_TOKENS— gives Claude more space to reason through complex problemsCLAUDE_CODE_MAX_OUTPUT_TOKENS— allows longer code generation responsesBASH_DEFAULT_TIMEOUT_MS— 10-minute timeout for slow builds (default is too short for large Next.js builds)permissions.allow— auto-approve Git commands, Bun commands, and file reads/edits so Claude doesn't ask for permission on every actionpermissions.deny— prevent reading sensitive files like secrets and credentialsdefaultMode: "acceptEdits"— automatically accept file edits (you can review them in Git)alwaysThinkingEnabled— enables extended thinking for better reasoningeffortLevel: "high"— Claude puts more effort into responses
Custom Status Line
Claude Code supports a custom status line that shows useful information at the bottom of the terminal. I use a script that displays the current directory, Git branch with diff stats, model name, thinking mode, version, and context usage percentage.
Create ~/.claude/statusline-command.sh:
#!/bin/bash
RESET="\033[0m"
CYAN="\033[36m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
MAGENTA="\033[35m"
ORANGE="\033[38;5;208m"
DIM="\033[2m"
# Read JSON input from Claude Code
input=$(cat)
# Extract values
cwd=$(echo "$input" | jq -r '.workspace.current_dir // ""')
dir=$(basename "$cwd")
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
version=$(echo "$input" | jq -r '.version // "unknown"')
# Check if thinking is enabled
settings_file="$HOME/.claude/settings.json"
if [ -f "$settings_file" ]; then
thinking_enabled=$(jq -r '.alwaysThinkingEnabled // false' "$settings_file")
else
thinking_enabled="false"
fi
if [ "$thinking_enabled" = "true" ]; then
thinking_indicator="🧠"
else
thinking_indicator="💭"
fi
# Context usage percentage
usage=$(echo "$input" | jq '.context_window.current_usage')
if [ "$usage" != "null" ]; then
current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens')
size=$(echo "$input" | jq '.context_window.context_window_size')
ctx=$((current * 100 / size))
else
ctx=0
fi
# Git info
if git -c core.fileMode=false rev-parse --git-dir > /dev/null 2>&1; then
branch=$(git -c core.fileMode=false branch --show-current 2>/dev/null || echo "?")
if [ ${#branch} -gt 15 ]; then
branch="${branch:0:12}..."
fi
stats=$(git -c core.fileMode=false diff --shortstat 2>/dev/null)
if [ -n "$stats" ]; then
adds=$(echo "$stats" | grep -o '[0-9]* insertion' | grep -o '[0-9]*')
dels=$(echo "$stats" | grep -o '[0-9]* deletion' | grep -o '[0-9]*')
adds=${adds:-0}
dels=${dels:-0}
git_info=$(printf ":${RED}%s${RESET} ${GREEN}+%s${RESET}${RED}-%s${RESET}" "$branch" "$adds" "$dels")
else
git_info=$(printf ":${RED}%s${RESET}" "$branch")
fi
else
git_info=""
fi
# Output: dir:branch +X-Y | Model 🧠 v1.0 | ctx%
printf "${CYAN}%s${RESET}%s ${DIM}|${RESET} ${MAGENTA}%s${RESET} %s ${ORANGE}v%s${RESET} ${DIM}|${RESET} ${YELLOW}%d%%${RESET}" \
"$dir" \
"$git_info" \
"$model_name" \
"$thinking_indicator" \
"$version" \
"$ctx"
Make it executable and add it to your settings:
chmod +x ~/.claude/statusline-command.sh
In ~/.claude/settings.json, add the status line configuration:
{
"statusLine": {
"type": "command",
"command": "/bin/bash /home/dev/.claude/statusline-command.sh"
}
}
Claude Code pipes JSON context (workspace info, model, context window usage) to the script's stdin. The script parses it with jq and outputs a colored status line showing everything at a glance — which project you're in, what branch, how many lines changed, which model is active, and how much context you've used.
The tmux + Claude Code Workflow
This is the core of the workflow. Here's how I structure my tmux sessions:
Session Layout
# Create a session for your project
tmux new-session -s myapp
# Window 0: Claude Code (main workspace)
# Window 1: Dev server
# Window 2: Shell (git, tests, etc.)
I keep Claude Code running in Window 0. When it needs to start a dev server, it sends the command to Window 1 via tmux send-keys. This keeps processes visible and controllable.
Starting a Dev Session
# SSH into your server
ssh hetzner
# Attach to your session (or create if it doesn't exist)
tmux attach -t myapp 2>/dev/null || tmux new-session -s myapp
# Navigate to your project
cd ~/projects/my-app
# Start Claude Code
claude
Disconnecting and Reconnecting
The beauty of this setup: you can close your laptop, go to a coffee shop, open your laptop, and pick up exactly where you left off:
ssh hetzner
tmux attach -t myapp
# Everything is exactly as you left it — Claude Code, dev server, terminal history
This works from any device. I've connected from my iPhone using Termius to check on a build that Claude Code was running.
Project-Level CLAUDE.md
Each project can have its own CLAUDE.md in the repo root with project-specific instructions:
# Project: My App
## Stack
- Next.js 16 with App Router
- Tailwind CSS v4
- Bun as package manager
## Commands
- `bun run dev` — start dev server on port 3000
- `bun run build` — production build
- `bun run lint` — run Biome linting
## Conventions
- Use Biome for formatting (not Prettier)
- Use Lefthook for git hooks
- Standalone output for production builds
Code Quality with Biome and Lefthook
I use Biome for linting and formatting (replacing ESLint + Prettier), and Lefthook for git hooks.
Biome Configuration
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 120,
"quoteStyle": "single"
},
"linter": { "enabled": true }
}
Lefthook Configuration
pre-commit:
commands:
biome:
glob: "*.{js,ts,tsx,json}"
stage_fixed: true
run: bun biome check --staged --write --no-errors-on-unmatched
This auto-formats staged files on commit. Claude Code respects these hooks — when it creates commits, Biome automatically formats the code.
Tips for Remote Development
1. Use Mosh for Unstable Connections
If you're on flaky WiFi, Mosh handles disconnects gracefully. Mosh uses UDP on high-numbered ports, which works over the Tailscale interface since we allowed all traffic on tailscale0 in Part 3:
# Install on server
sudo apt install mosh
# Connect (via Tailscale IP)
mosh --ssh="ssh -p 1993" dev@YOUR_TAILSCALE_IP
2. Keep Your Dev Server in tmux
Never let Claude Code or any process start dev servers in the background. Always use tmux windows:
# From Claude Code's perspective, it should do:
tmux send-keys -t myapp:1 "bun run dev" Enter
# NOT:
bun run dev & # This creates zombie processes
3. VS Code in the Browser with code-server
Instead of using VS Code's Remote SSH extension, I run code-server — a full VS Code instance that runs on the server and is accessible from any browser. No local VS Code installation needed. You can code from a tablet, a borrowed laptop, or any device with a browser.
Install code-server
curl -fsSL https://code-server.dev/install.sh | sh
This installs the code-server binary system-wide. Enable and start it as a systemd service for your user:
sudo systemctl enable --now code-server@$USER
Configure code-server
The config file is at ~/.config/code-server/config.yaml:
bind-addr: 127.0.0.1:8080
auth: password
password: your-secure-password-here
cert: false
Key settings:
bind-addr: 127.0.0.1:8080— only listens locally, Nginx handles public accessauth: password— requires a password to access the editorcert: false— Nginx + Certbot handles SSL, not code-server
After editing the config, restart the service:
sudo systemctl restart code-server@$USER
Set Up Nginx Reverse Proxy
Create an Nginx config to expose code-server on a subdomain:
sudo tee /etc/nginx/sites-available/code-server << 'EOF'
server {
server_name code.myapp.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Accept-Encoding gzip;
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;
}
listen 80;
}
EOF
sudo ln -s /etc/nginx/sites-available/code-server /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
The Upgrade and Connection headers are essential — code-server uses WebSockets for the editor's real-time features.
Add SSL with Certbot
sudo certbot --nginx -d code.myapp.com
Certbot automatically configures HTTPS and sets up HTTP → HTTPS redirects.
Install Extensions
code-server supports VS Code extensions. Install them from the command line:
# Essentials
code-server --install-extension biomejs.biome
code-server --install-extension bradlc.vscode-tailwindcss
code-server --install-extension prisma.prisma
# Theme
code-server --install-extension zhuangtongfa.material-theme
code-server --install-extension pkief.material-icon-theme
# Claude Code extension
code-server --install-extension anthropic.claude-code
Now you can open https://code.myapp.com from any browser, enter your password, and you have a full VS Code editor running on your server. I use this when I'm away from my main machine — it's a complete development environment accessible from anywhere.
4. Connect from Any Device with Termius
The whole point of a remote dev server is working from anywhere. I use Termius as my SSH client across all my devices — Mac, iPad, and iPhone. It's the glue that makes this setup truly portable.
Why Termius
- Cross-device sync — set up your server connection once, and it's available on every device automatically
- Built-in port forwarding — preview your dev server from any device without command-line SSH tunnels
- SFTP — browse and transfer files visually when needed
- Snippets — save common commands (like
tmux attach -t website) and run them with a tap
Setting Up Your Connection
In Termius, create a new host:
| Field | Value |
|---|---|
| Label | hetzner |
| Hostname | Your server's Tailscale IP (100.x.y.z) |
| Port | 1993 |
| Username | dev |
| Key | Import your SSH private key |
Save it once, and it syncs to your Mac, iPad, and iPhone instantly. No re-entering credentials on each device.
Port Forwarding for Dev Server Preview
When your Next.js dev server is running on the server (port 3000), you need port forwarding to preview it in your local browser. In Termius:
- Open your
hetznerhost settings - Go to Port Forwarding
- Add a new rule:
- Type: Local
- Local port:
3000 - Remote host:
127.0.0.1 - Remote port:
3000
Now visit http://localhost:3000 on your device and you'll see your dev server. This works on Mac, iPad, and iPhone — Termius handles the SSH tunnel transparently.
You can add multiple forwarding rules for different services:
| Local Port | Remote | Use |
|---|---|---|
3000 | 127.0.0.1:3000 | Next.js dev server |
5432 | 127.0.0.1:5432 | PostgreSQL |
8080 | 127.0.0.1:8080 | code-server |
The Multi-Device Workflow
Here's what working from different devices looks like in practice:
From my Mac — full development. Termius connects to the server, I attach to tmux, run Claude Code, and use port forwarding to preview in Chrome.
From my iPad — lighter work. I open Termius, attach to the existing tmux session, and everything is exactly where I left it. Claude Code is still running. The dev server is still up. I can review changes, run builds, or make quick edits.
From my iPhone — quick checks. Tap the hetzner connection in Termius, tmux attach -t website, check if a build passed or review Claude Code's output. It's surprisingly usable for monitoring.
The key insight: the server is always running. Termius is just a window into it. You're not "transferring work" between devices — you're looking at the same environment from different screens.
Real-World Example
Here's what a typical session looks like when I'm building a feature:
# 1. Open Termius and connect to hetzner
# (or from terminal: ssh hetzner)
# 2. Attach to the project session
tmux attach -t website
# 3. Start Claude Code in window 0
claude
# 4. Tell Claude what to build
> Add a dark mode toggle to the navbar
# Claude reads my codebase, understands the Tailwind setup,
# creates the component, installs any needed packages,
# and tests the build — all on the server.
# 5. Dev server is already running in window 1
# Preview via Termius port forwarding at localhost:3000
# 6. When done, close the laptop
# Everything keeps running on the server
# Later, open Termius on iPad and pick up where you left off
What's Next
We have a fully functional, secure development environment with AI assistance. In the final part, we'll set up production deployment — Nginx reverse proxy, PM2 process management, Let's Encrypt SSL, and automated deploys with GitHub Actions.
Series Navigation
- Choosing Your Server
- Initial Server Setup
- Security with Tailscale VPN
- Claude Code & Development Workflow (you are here)
- Production Deployment with GitHub Actions & PM2