# Remote Dev Server Guide Part 4: Claude Code & Development Workflow Date: 2026-03-08 · 13 min read Tags: claude-code, ai, development, hetzner, remote-development URL: https://ahmedelywa.com/blog/hetzner-remote-dev-part-4-claude-code-dev-workflow --- In [Part 3](/blog/hetzner-remote-dev-part-3-security-with-tailscale), 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: ```bash 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: ```bash 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: ```markdown # 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: ```json { "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 problems - **`CLAUDE_CODE_MAX_OUTPUT_TOKENS`** — allows longer code generation responses - **`BASH_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 action - **`permissions.deny`** — prevent reading sensitive files like secrets and credentials - **`defaultMode: "acceptEdits"`** — automatically accept file edits (you can review them in Git) - **`alwaysThinkingEnabled`** — enables extended thinking for better reasoning - **`effortLevel: "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`: ```bash #!/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: ```bash chmod +x ~/.claude/statusline-command.sh ``` In `~/.claude/settings.json`, add the status line configuration: ```json { "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 ```bash # 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 ```bash # 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: ```bash 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: ```markdown # 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](https://biomejs.dev) for linting and formatting (replacing ESLint + Prettier), and [Lefthook](https://github.com/evilmartians/lefthook) for git hooks. ### Biome Configuration ```json { "$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 ```yaml 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](https://mosh.org) 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: ```bash # 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: ```bash # 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](https://github.com/coder/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 ```bash 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: ```bash sudo systemctl enable --now code-server@$USER ``` #### Configure code-server The config file is at `~/.config/code-server/config.yaml`: ```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 access - **`auth: password`** — requires a password to access the editor - **`cert: false`** — Nginx + Certbot handles SSL, not code-server After editing the config, restart the service: ```bash sudo systemctl restart code-server@$USER ``` #### Set Up Nginx Reverse Proxy Create an Nginx config to expose code-server on a subdomain: ```bash 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 ```bash 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: ```bash # 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](https://termius.com) 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: 1. Open your `hetzner` host settings 2. Go to **Port Forwarding** 3. 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: ```bash # 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](/blog/hetzner-remote-dev-part-5-production-deployment), we'll set up production deployment — Nginx reverse proxy, PM2 process management, Let's Encrypt SSL, and automated deploys with GitHub Actions. ## Series Navigation 1. [Choosing Your Server](/blog/hetzner-remote-dev-part-1-choosing-your-server) 2. [Initial Server Setup](/blog/hetzner-remote-dev-part-2-initial-server-setup) 3. [Security with Tailscale VPN](/blog/hetzner-remote-dev-part-3-security-with-tailscale) 4. **Claude Code & Development Workflow** (you are here) 5. [Production Deployment with GitHub Actions & PM2](/blog/hetzner-remote-dev-part-5-production-deployment)