Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gstack/no-test-bootstrap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.0.2.0] - 2026-04-14

### Added

- `server/terminal/start-claude-sessions.sh`: boot script that auto-starts one tmux session per git repo in `/root` on server reboot. Each session cds into the repo and runs `happy claude` (swappable with `claude` if not using Happy). Install via `@reboot` cron. Idempotent — skips repos that already have a running session.
- `server/terminal/README.md`: documented Happy (mobile/web access to headless Claude Code sessions), the boot script, and installation steps for both.
- `server/README.md`: Quick Deploy steps 6 (auto-start on boot) and 7 (Happy mobile access).
- `mac/zshrc`: Mac-side shell template with `dev()` iTerm2 multi-tab function (opens one tab per server tmux session) and `devs` alias (list sessions without attaching).

## [0.0.1.0] - 2026-04-14

### Added

- `server/setup-claude-auth.sh`: one-command script that pushes Claude Code auth from a Mac to a headless Linux dev server. Extracts OAuth credentials from the macOS Keychain, copies them to the server as `~/.claude/.credentials.json`, removes any stale `ANTHROPIC_AUTH_TOKEN` from `~/.bashrc` (Claude Code reads the credentials file natively — that env var is for API keys only and causes 401 errors with OAuth), and marks onboarding complete in `~/.claude.json` so the theme picker never blocks startup. Replaces a painful multi-step manual process.
- `server/README.md`: Quick Deploy step 0 documenting the new auth setup flow for Max/Pro plan users, with an API key fallback note.

### Changed

- `claude/hooks/enforce-hetzner-heavy-tasks.sh`: added `dev` to the SSH alias allowlist and anchored the regex with a word boundary so `ssh developer` no longer matches.
- `CLAUDE.md`: added gstack skill routing rules (enables proactive skill dispatch in this repo).

### Security

- Credentials file written with `umask 0077` + `chmod 600` for defense-in-depth against world-readable exposure window.
- `mkdir -p` and credential write combined into a single SSH session to eliminate symlink TOCTOU.
- `sed` pattern scoped to `^export ANTHROPIC_AUTH_TOKEN=` to avoid clobbering unrelated `.bashrc` lines.
- `ConnectTimeout=10` added to all SSH calls to prevent hangs in automation contexts.
- `~/.claude.json` permissions set to `0o600` after write.
- Empty auth status response now caught explicitly with a clear error message.
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Skill routing

When the user's request matches an available skill, ALWAYS invoke it using the Skill
tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.
The skill has specialized workflows that produce better results than ad-hoc answers.

Key routing rules:
- Product ideas, "is this worth building", brainstorming → invoke office-hours
- Bugs, errors, "why is this broken", 500 errors → invoke investigate
- Ship, deploy, push, create PR → invoke ship
- QA, test the site, find bugs → invoke qa
- Code review, check my diff → invoke review
- Update docs after shipping → invoke document-release
- Weekly retro → invoke retro
- Design system, brand → invoke design-consultation
- Visual audit, design polish → invoke design-review
- Architecture review → invoke plan-eng-review
- Save progress, checkpoint, resume → invoke checkpoint
- Code quality, health check → invoke health
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Templates for running Claude Code on a dedicated dev server (VPS/dedicated). Not
<details>
<summary><strong>What's in server/</strong></summary>

- **Auth setup** - `setup-claude-auth.sh`: one-command Mac-to-server OAuth push for Claude Code Max/Pro plans
- **Systemd services** - Chrome headless with CDP, SSHFS mounts, CDP keepalive, Docker-host proxy
- **Safety utilities** - `safe-pipeline` (flock + timeout + cgroup memory cap), `safe-run`, stale process cleanup
- **Browser automation** - Chrome CDP setup guide, bridge keeper for session persistence
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0.2.0
2 changes: 1 addition & 1 deletion claude/hooks/enforce-hetzner-heavy-tasks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Allow SSH commands that delegate TO servers (the correct pattern)
# Customize: replace with your actual server aliases
if echo "$COMMAND" | grep -qiE "^ssh (devserver|hetzner|ax41)"; then
if echo "$COMMAND" | grep -qiE "^ssh (dev|devserver|hetzner|ax41)(\s|$)"; then
exit 0
fi

Expand Down
35 changes: 35 additions & 0 deletions mac/zshrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ~/.zshrc additions for Mac — Claude Code dev server workflow
#
# Sanitized template. Add to your ~/.zshrc or source this file directly:
# source ~/path/to/claude-setup/mac/zshrc
#
# Requires: iTerm2, SSH alias "dev" configured in ~/.ssh/config

# --- Dev server connection ---
#
# Single command to connect to all running tmux sessions on the dev server.
# Opens one iTerm2 tab per tmux session, each attached to its own session.
# Falls back to creating a new "main" session if none exist.
#
# Usage: dev
dev() {
local sessions
sessions=$(ssh dev "tmux ls -F '#{session_name}'" 2>/dev/null)
if [ -z "$sessions" ]; then
ssh dev -t "tmux new -s main"
return
fi
local first rest
first=$(echo "$sessions" | head -1)
rest=$(echo "$sessions" | tail -n +2)
# Open each additional session in a new iTerm2 tab
echo "$rest" | while read -r s; do
[ -z "$s" ] && continue
osascript -e "tell application \"iTerm2\" to tell current window to create tab with default profile command \"ssh dev -t 'tmux attach -t $s'\""
done
# Attach current tab to first session
ssh dev -t "tmux attach -t $first"
}

# List running tmux sessions on the dev server (no attach)
alias devs="ssh dev -t 'tmux ls'"
75 changes: 75 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

This directory contains everything needed to turn a dedicated Linux server into a productive Claude Code environment with browser automation, safe execution wrappers, SSHFS mounts, and multi-account tooling.

## Workflow

The setup this repo encodes:

```
Mac (iTerm2)
└── dev() ──── SSH ──── Linux dev server (49.213.x.x or similar)
├── tmux: fitness-assessment → happy claude
├── tmux: floom-internal → happy claude
└── tmux: second-brain → happy claude

Mobile (iOS/Android)
└── Happy app ──── HTTPS ──── same tmux sessions (via happy.slopus.com)
```

**What this gives you:**
- One command (`dev`) from Mac opens one iTerm2 tab per running tmux session, each attached
- Sessions persist through SSH disconnects — close your laptop, work continues
- Mobile access via Happy: send prompts, watch output, get notified when Claude is done
- On every server reboot, sessions auto-start per git repo and launch `happy claude`
- Auth is managed by `setup-claude-auth.sh` — push credentials from Mac Keychain, no OAuth pain

**Day-to-day:**
```bash
dev # connect from Mac — opens one tab per session
devs # list sessions without attaching
ssh dev # plain SSH if you prefer
```

**Re-authentication** (credentials expire ~1 year):
```bash
./server/setup-claude-auth.sh dev
```

---

## Prerequisites

- Ubuntu 22.04+ or Debian 12+
Expand Down Expand Up @@ -29,6 +65,21 @@ server/

## Quick Deploy

### 0. Authenticate Claude Code (Max/Pro plan, from your Mac)

Logging in to Claude Code on a headless Linux server via the normal OAuth flow is broken: the authorization code gets mangled on paste, and even when it succeeds the TUI shows a sign-in screen on every startup because `hasCompletedOnboarding` is missing from `~/.claude.json`.

The fix: push credentials from your Mac directly.

```bash
# Run from your Mac, after logging in to Claude Code locally
./setup-claude-auth.sh dev # replace "dev" with your SSH alias
```

This copies your OAuth credentials from the macOS Keychain to the server and marks onboarding complete in `~/.claude.json`. Claude Code reads the credentials file natively — no env var needed. After that, `ssh dev && claude` works with no prompts.

If you're using an API key instead of a Max/Pro plan, skip this step and set `ANTHROPIC_API_KEY` in the server's `~/.bashrc`.

### 1. Install safety wrappers
```bash
cp safety/safe-pipeline /usr/local/bin/safe-pipeline
Expand Down Expand Up @@ -71,6 +122,30 @@ systemctl enable --now chrome-bridge-keeper
```bash
cp terminal/tmux.conf ~/.tmux.conf
# Add useful aliases from terminal/ scripts to your ~/.bashrc
cp server/bashrc ~/.bashrc # or source it from your existing ~/.bashrc
```

### 6. Auto-start sessions on boot (optional)

Starts one tmux session per git repo in `/root` on every server reboot, each running `happy claude`:

```bash
cp terminal/start-claude-sessions.sh /usr/local/bin/start-claude-sessions.sh
chmod +x /usr/local/bin/start-claude-sessions.sh
(crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/start-claude-sessions.sh") | crontab -
```

Replace `happy claude` with `claude` if you're not using Happy for mobile access.

### 7. Mobile access with Happy (optional)

[Happy](https://github.com/slopus/happy) gives you a mobile/web interface to control
Claude Code sessions on the server.

```bash
npm install -g happy
# Then start sessions with: happy claude
# Or set as default in ~/.tmux.conf: set -g default-command "happy claude"
```

## Key Concepts
Expand Down
132 changes: 132 additions & 0 deletions server/setup-claude-auth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/bin/bash
# setup-claude-auth.sh - Push Claude Code auth from your Mac to a Linux dev server
#
# Usage: ./setup-claude-auth.sh [ssh-alias]
# ssh-alias defaults to "dev"
#
# Run this from your Mac after provisioning a new server.
# Requires: Claude Code installed and logged in on this Mac.
#
# What it does:
# 1. Extracts your OAuth credentials from the macOS Keychain
# 2. Copies them to the server as ~/.claude/.credentials.json
# 3. Removes any stale ANTHROPIC_AUTH_TOKEN from ~/.bashrc (that env var is for API keys only)
# 4. Marks onboarding complete in ~/.claude.json so the theme picker doesn't block startup
#
# Background: Claude Code's headless OAuth flow on Linux is broken — the authorization
# code gets mangled on paste, and even when auth succeeds the TUI shows a sign-in screen
# because hasCompletedOnboarding is missing from ~/.claude.json. This script bypasses
# the entire flow by pushing credentials directly from the Mac Keychain.

set -euo pipefail

SERVER="${1:-dev}"

# Validate SSH alias — only allow safe hostname/alias characters
if ! [[ "$SERVER" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo -e "\033[0;31m[ERROR]\033[0m Invalid SSH alias: '$SERVER'. Use only letters, numbers, dots, hyphens, underscores." >&2
exit 1
fi

RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'

info() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }

# --- 1. Extract credentials from macOS Keychain ---

info "Reading Claude Code credentials from macOS Keychain..."

CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || true)
if [ -z "$CREDS" ]; then
err "No Claude Code credentials found in Keychain. Log in to Claude Code on this Mac first, then re-run."
fi

# Validate that the JSON parses and contains the expected key
if ! echo "$CREDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
if 'claudeAiOauth' not in d or 'accessToken' not in d['claudeAiOauth']:
raise KeyError('claudeAiOauth.accessToken missing')
" 2>/dev/null; then
err "Could not parse access token from credentials. Keychain entry may be malformed. Try: security find-generic-password -s 'Claude Code-credentials' -w"
fi

ok "Credentials extracted."

# --- 2. Verify SSH connection ---

info "Connecting to $SERVER..."
ssh -o ConnectTimeout=10 "$SERVER" "echo ok" > /dev/null 2>&1 || err "Cannot connect to $SERVER. Check your SSH config."
ok "Connected to $SERVER"

# --- 3. Copy credentials file ---

info "Copying credentials to $SERVER:~/.claude/.credentials.json..."
printf '%s\n' "$CREDS" | ssh -o ConnectTimeout=10 "$SERVER" "umask 0077; mkdir -p ~/.claude; cat > ~/.claude/.credentials.json; chmod 600 ~/.claude/.credentials.json"
ok ".credentials.json installed"

# --- 4. Remove any stale ANTHROPIC_AUTH_TOKEN from .bashrc ---
# Claude Code reads ~/.claude/.credentials.json natively for Max/Pro plan auth.
# Setting ANTHROPIC_AUTH_TOKEN to an OAuth token causes "OAuth authentication is
# currently not supported" errors because that env var is only for API keys.

info "Removing any stale ANTHROPIC_AUTH_TOKEN from ~/.bashrc on $SERVER..."
ssh -o ConnectTimeout=10 "$SERVER" "sed -i '/^export ANTHROPIC_AUTH_TOKEN=/d' ~/.bashrc"
ok "ANTHROPIC_AUTH_TOKEN cleared from ~/.bashrc"

# --- 5. Mark onboarding complete in ~/.claude.json ---
# Without this, Claude Code shows a theme picker on every startup because
# hasCompletedOnboarding is absent from the config, regardless of auth state.

info "Marking onboarding complete on $SERVER..."
ssh -o ConnectTimeout=10 "$SERVER" "python3 - <<'PYEOF'
import json, os, subprocess

path = os.path.expanduser('~/.claude.json')
d = json.load(open(path)) if os.path.exists(path) else {}

try:
ver = subprocess.check_output(['claude', '--version'], text=True).split()[0]
except Exception:
ver = 'unknown'

d['hasCompletedOnboarding'] = True
d['lastOnboardingVersion'] = ver

with open(path, 'w') as f:
json.dump(d, f, indent=2)
os.chmod(path, 0o600)

print(f'Onboarding marked complete for Claude Code {ver}')
PYEOF"
ok "~/.claude.json updated"

# --- 6. Verify ---

info "Verifying auth on $SERVER..."
RESULT=$(ssh -o ConnectTimeout=10 "$SERVER" "claude auth status 2>/dev/null" 2>/dev/null || true)
if [ -z "$RESULT" ]; then
err "Auth verification failed: 'claude auth status' returned no output. Is Claude Code installed on $SERVER?"
fi
if echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('loggedIn') else 1)" 2>/dev/null; then
EMAIL=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('email','unknown'))" 2>/dev/null || echo "unknown")
ok "Authenticated as $EMAIL"
else
echo "Auth status: $RESULT"
err "Auth verification failed. Try running 'claude auth status' on the server manually."
fi

echo ""
echo "============================================"
echo " Claude Code auth configured on $SERVER"
echo "============================================"
echo ""
echo " ssh $SERVER"
echo " claude"
echo ""
echo "Note: The credentials file expires in ~1 year. Re-run this script to refresh."
46 changes: 46 additions & 0 deletions server/terminal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,52 @@ cq add -s backend "Add rate limiting to /api/users"
cq status
```

## Auto-Start on Boot (start-claude-sessions.sh)

Automatically starts one tmux session per git repo when the server boots.
Each session cds into the repo and runs `happy claude`.

**Install:**
```bash
cp start-claude-sessions.sh /usr/local/bin/start-claude-sessions.sh
chmod +x /usr/local/bin/start-claude-sessions.sh

# Add to crontab:
(crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/start-claude-sessions.sh") | crontab -
```

**How it works:**

- Scans `/root` for subdirectories containing a `.git` folder (maxdepth 2)
- Creates a named tmux session for each repo (named after the directory)
- Sends `happy claude` to each new session
- Idempotent: skips repos that already have a running session

**Replace `happy claude` with `claude`** if you're not using Happy for mobile access.

## Happy (Mobile Access)

[Happy](https://github.com/slopus/happy) provides a mobile and web interface to
control Claude Code sessions running on a headless server.

**Install:**
```bash
npm install -g happy
```

**Usage:**
```bash
# Start a session with mobile access enabled
happy claude

# Or as the tmux default-command (in tmux.conf):
# set -g default-command "happy claude"
```

**Self-hosting vs. cloud:** The default Happy backend is the cloud service at
`happy.slopus.com`. For self-hosted, follow the [Happy server setup guide](https://github.com/slopus/happy).
The cloud option is simpler and fine for personal use; self-hosted gives full control.

## Aliases

Useful shell aliases to add to `~/.bashrc`:
Expand Down
Loading