diff --git a/.gstack/no-test-bootstrap b/.gstack/no-test-bootstrap new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.gstack/no-test-bootstrap @@ -0,0 +1 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5ab4903 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1da9cc9 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index 8c3b07b..fb615ea 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ Templates for running Claude Code on a dedicated dev server (VPS/dedicated). Not
What's in server/ +- **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 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..b456a71 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.2.0 diff --git a/claude/hooks/enforce-hetzner-heavy-tasks.sh b/claude/hooks/enforce-hetzner-heavy-tasks.sh index e239932..85a47f9 100644 --- a/claude/hooks/enforce-hetzner-heavy-tasks.sh +++ b/claude/hooks/enforce-hetzner-heavy-tasks.sh @@ -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 diff --git a/mac/zshrc b/mac/zshrc new file mode 100644 index 0000000..72d2013 --- /dev/null +++ b/mac/zshrc @@ -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'" diff --git a/server/README.md b/server/README.md index 36b2e30..561c0b9 100644 --- a/server/README.md +++ b/server/README.md @@ -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+ @@ -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 @@ -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 diff --git a/server/setup-claude-auth.sh b/server/setup-claude-auth.sh new file mode 100755 index 0000000..9e1648c --- /dev/null +++ b/server/setup-claude-auth.sh @@ -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." diff --git a/server/terminal/README.md b/server/terminal/README.md index c920721..1f29638 100644 --- a/server/terminal/README.md +++ b/server/terminal/README.md @@ -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`: diff --git a/server/terminal/start-claude-sessions.sh b/server/terminal/start-claude-sessions.sh new file mode 100644 index 0000000..da220e2 --- /dev/null +++ b/server/terminal/start-claude-sessions.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# start-claude-sessions.sh - Auto-start one tmux session per git repo on boot +# +# Usage: Install via cron: +# @reboot /usr/local/bin/start-claude-sessions.sh +# +# On boot, finds every git repo under /root (maxdepth 2), creates a named +# tmux session for each one (named after the repo directory), cds into it, +# and runs "happy claude" to start a Claude Code session with mobile access. +# +# Idempotent: skips repos that already have a running tmux session. +# +# Requires: +# - tmux +# - happy (npm install -g happy) or replace with: claude +# - Git repos as direct subdirectories of /root + +set -euo pipefail + +# Small delay to let the system fully boot before spawning sessions +sleep 5 + +find /root -maxdepth 2 -name ".git" -type d 2>/dev/null \ + | sed 's|/.git$||' \ + | sort \ + | while read -r repo; do + name=$(basename "$repo") + # Skip if a session with this name already exists + if tmux has-session -t "$name" 2>/dev/null; then + continue + fi + tmux new-session -d -s "$name" -c "$repo" "happy claude" + done