From 431f6d26fc3b68acd1479bd69568187a0a1edbb1 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 11:57:24 +0200 Subject: [PATCH 01/12] chore: add gstack skill routing rules to CLAUDE.md --- CLAUDE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CLAUDE.md 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 From b540c93d4d5188da6051e56c44f13bb6da702b1b Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 11:57:50 +0200 Subject: [PATCH 02/12] feat: add setup-claude-auth.sh script and server README docs Automates pushing Claude Code OAuth credentials from Mac to Linux dev server. Updates enforce-hetzner hook to recognize 'dev' SSH alias. Documents the headless server auth setup in server/README.md. --- claude/hooks/enforce-hetzner-heavy-tasks.sh | 2 +- server/README.md | 15 +++ server/setup-claude-auth.sh | 126 ++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100755 server/setup-claude-auth.sh diff --git a/claude/hooks/enforce-hetzner-heavy-tasks.sh b/claude/hooks/enforce-hetzner-heavy-tasks.sh index e239932..dce809d 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)"; then exit 0 fi diff --git a/server/README.md b/server/README.md index 36b2e30..dde8d12 100644 --- a/server/README.md +++ b/server/README.md @@ -29,6 +29,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 token from the macOS Keychain to the server, sets `ANTHROPIC_AUTH_TOKEN` in `~/.bashrc`, and marks onboarding complete in `~/.claude.json`. 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 diff --git a/server/setup-claude-auth.sh b/server/setup-claude-auth.sh new file mode 100755 index 0000000..bae54f6 --- /dev/null +++ b/server/setup-claude-auth.sh @@ -0,0 +1,126 @@ +#!/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. Sets ANTHROPIC_AUTH_TOKEN in ~/.bashrc (suppresses the interactive sign-in screen) +# 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}" + +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"; 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 + +ACCESS_TOKEN=$(echo "$CREDS" | python3 -c " +import sys, json +d = json.load(sys.stdin) +print(d['claudeAiOauth']['accessToken']) +" 2>/dev/null || true) + +if [ -z "$ACCESS_TOKEN" ]; then + err "Could not parse access token from credentials. Keychain entry may be malformed." +fi + +ok "Credentials extracted (token: ${ACCESS_TOKEN:0:20}...)" + +# --- 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..." +ssh "$SERVER" "mkdir -p ~/.claude" +echo "$CREDS" | ssh "$SERVER" "cat > ~/.claude/.credentials.json && chmod 600 ~/.claude/.credentials.json" +ok ".credentials.json installed" + +# --- 4. Set ANTHROPIC_AUTH_TOKEN in .bashrc --- +# This tells Claude Code's interactive TUI that auth is already handled, +# suppressing the sign-in screen even before the credentials file is read. + +info "Setting ANTHROPIC_AUTH_TOKEN in ~/.bashrc on $SERVER..." +ssh "$SERVER" " + # Remove any stale entry first + sed -i '/ANTHROPIC_AUTH_TOKEN/d' ~/.bashrc + echo 'export ANTHROPIC_AUTH_TOKEN=\"${ACCESS_TOKEN}\"' >> ~/.bashrc +" +ok "ANTHROPIC_AUTH_TOKEN set in ~/.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 "$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) + +print(f'Onboarding marked complete for Claude Code {ver}') +PYEOF" +ok "~/.claude.json updated" + +# --- 6. Verify --- + +info "Verifying auth on $SERVER..." +RESULT=$(ssh "$SERVER" "bash -i -c 'claude auth status 2>/dev/null'" 2>/dev/null || true) +if echo "$RESULT" | grep -q '"loggedIn": true'; 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 OAuth token expires in ~1 year. Re-run this script to refresh." From 2978d9de48adc5200a4c4e77d1aeca155ebf2a87 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 12:03:52 +0200 Subject: [PATCH 03/12] fix: pre-landing review security fixes - Validate $SERVER arg against safe hostname charset - Remove OAuth token prefix from log output - Use printf instead of echo for credentials pipe - Store ANTHROPIC_AUTH_TOKEN dynamically in .bashrc (reads from credentials file, no plaintext token) - Remove -i flag from auth verification bash (non-interactive) - Replace fragile grep JSON check with python3 parse --- .gstack/no-test-bootstrap | 1 + server/setup-claude-auth.sh | 52 ++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 .gstack/no-test-bootstrap 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/server/setup-claude-auth.sh b/server/setup-claude-auth.sh index bae54f6..fe203d5 100755 --- a/server/setup-claude-auth.sh +++ b/server/setup-claude-auth.sh @@ -10,7 +10,7 @@ # What it does: # 1. Extracts your OAuth credentials from the macOS Keychain # 2. Copies them to the server as ~/.claude/.credentials.json -# 3. Sets ANTHROPIC_AUTH_TOKEN in ~/.bashrc (suppresses the interactive sign-in screen) +# 3. Sets ANTHROPIC_AUTH_TOKEN in ~/.bashrc (reads from credentials file at login) # 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 @@ -22,6 +22,12 @@ 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' @@ -29,7 +35,7 @@ NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $1"; } ok() { echo -e "${GREEN}[OK]${NC} $1"; } -err() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +err() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; } # --- 1. Extract credentials from macOS Keychain --- @@ -40,17 +46,17 @@ 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 -ACCESS_TOKEN=$(echo "$CREDS" | python3 -c " +# Validate that the JSON parses and contains the expected key +if ! echo "$CREDS" | python3 -c " import sys, json d = json.load(sys.stdin) -print(d['claudeAiOauth']['accessToken']) -" 2>/dev/null || true) - -if [ -z "$ACCESS_TOKEN" ]; then - err "Could not parse access token from credentials. Keychain entry may be malformed." +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 (token: ${ACCESS_TOKEN:0:20}...)" +ok "Credentials extracted." # --- 2. Verify SSH connection --- @@ -62,20 +68,24 @@ ok "Connected to $SERVER" info "Copying credentials to $SERVER:~/.claude/.credentials.json..." ssh "$SERVER" "mkdir -p ~/.claude" -echo "$CREDS" | ssh "$SERVER" "cat > ~/.claude/.credentials.json && chmod 600 ~/.claude/.credentials.json" +printf '%s\n' "$CREDS" | ssh "$SERVER" "cat > ~/.claude/.credentials.json && chmod 600 ~/.claude/.credentials.json" ok ".credentials.json installed" # --- 4. Set ANTHROPIC_AUTH_TOKEN in .bashrc --- -# This tells Claude Code's interactive TUI that auth is already handled, -# suppressing the sign-in screen even before the credentials file is read. +# Read the token dynamically from the credentials file at login rather than storing +# the literal token value. This keeps ~/.bashrc free of credential plaintext and +# automatically picks up refreshed tokens without re-running this script. info "Setting ANTHROPIC_AUTH_TOKEN in ~/.bashrc on $SERVER..." -ssh "$SERVER" " - # Remove any stale entry first - sed -i '/ANTHROPIC_AUTH_TOKEN/d' ~/.bashrc - echo 'export ANTHROPIC_AUTH_TOKEN=\"${ACCESS_TOKEN}\"' >> ~/.bashrc -" -ok "ANTHROPIC_AUTH_TOKEN set in ~/.bashrc" +# Use bash -s + heredoc to safely inject the dynamic token line without quoting nightmares. +# The token is sourced from the credentials file at each login — no literal token in .bashrc. +ssh "$SERVER" 'bash -s' <<'REMOTESCRIPT' +sed -i '/ANTHROPIC_AUTH_TOKEN/d' ~/.bashrc +cat >> ~/.bashrc <<'BASHLINE' +export ANTHROPIC_AUTH_TOKEN=$(python3 -c "import json,os; d=json.load(open(os.path.expanduser('~/.claude/.credentials.json'))); print(d['claudeAiOauth']['accessToken'])" 2>/dev/null) +BASHLINE +REMOTESCRIPT +ok "ANTHROPIC_AUTH_TOKEN set in ~/.bashrc (reads from credentials file)" # --- 5. Mark onboarding complete in ~/.claude.json --- # Without this, Claude Code shows a theme picker on every startup because @@ -106,8 +116,8 @@ ok "~/.claude.json updated" # --- 6. Verify --- info "Verifying auth on $SERVER..." -RESULT=$(ssh "$SERVER" "bash -i -c 'claude auth status 2>/dev/null'" 2>/dev/null || true) -if echo "$RESULT" | grep -q '"loggedIn": true'; then +RESULT=$(ssh "$SERVER" "claude auth status 2>/dev/null" 2>/dev/null || true) +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 @@ -123,4 +133,4 @@ echo "" echo " ssh $SERVER" echo " claude" echo "" -echo "Note: The OAuth token expires in ~1 year. Re-run this script to refresh." +echo "Note: The credentials file expires in ~1 year. Re-run this script to refresh." From 90a7ca0ac9c6e6dab7643eb79b67adcf09b6010a Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 12:10:20 +0200 Subject: [PATCH 04/12] fix: pre-landing specialist review fixes (bashrc guard, claude.json chmod, empty auth check) --- server/setup-claude-auth.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/setup-claude-auth.sh b/server/setup-claude-auth.sh index fe203d5..98b74a7 100755 --- a/server/setup-claude-auth.sh +++ b/server/setup-claude-auth.sh @@ -80,6 +80,7 @@ info "Setting ANTHROPIC_AUTH_TOKEN in ~/.bashrc on $SERVER..." # Use bash -s + heredoc to safely inject the dynamic token line without quoting nightmares. # The token is sourced from the credentials file at each login — no literal token in .bashrc. ssh "$SERVER" 'bash -s' <<'REMOTESCRIPT' +touch ~/.bashrc sed -i '/ANTHROPIC_AUTH_TOKEN/d' ~/.bashrc cat >> ~/.bashrc <<'BASHLINE' export ANTHROPIC_AUTH_TOKEN=$(python3 -c "import json,os; d=json.load(open(os.path.expanduser('~/.claude/.credentials.json'))); print(d['claudeAiOauth']['accessToken'])" 2>/dev/null) @@ -108,6 +109,7 @@ 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" @@ -117,6 +119,9 @@ ok "~/.claude.json updated" info "Verifying auth on $SERVER..." RESULT=$(ssh "$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" From cab60b1fc61abb055e30665720f75e9b12186f45 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 12:13:54 +0200 Subject: [PATCH 05/12] fix: adversarial review fixes (umask+TOCTOU, sed pattern, SSH timeouts, hook word boundary) --- claude/hooks/enforce-hetzner-heavy-tasks.sh | 2 +- server/setup-claude-auth.sh | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/claude/hooks/enforce-hetzner-heavy-tasks.sh b/claude/hooks/enforce-hetzner-heavy-tasks.sh index dce809d..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 (dev|devserver|hetzner|ax41)"; then +if echo "$COMMAND" | grep -qiE "^ssh (dev|devserver|hetzner|ax41)(\s|$)"; then exit 0 fi diff --git a/server/setup-claude-auth.sh b/server/setup-claude-auth.sh index 98b74a7..f961217 100755 --- a/server/setup-claude-auth.sh +++ b/server/setup-claude-auth.sh @@ -67,8 +67,7 @@ ok "Connected to $SERVER" # --- 3. Copy credentials file --- info "Copying credentials to $SERVER:~/.claude/.credentials.json..." -ssh "$SERVER" "mkdir -p ~/.claude" -printf '%s\n' "$CREDS" | ssh "$SERVER" "cat > ~/.claude/.credentials.json && chmod 600 ~/.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. Set ANTHROPIC_AUTH_TOKEN in .bashrc --- @@ -79,9 +78,9 @@ ok ".credentials.json installed" info "Setting ANTHROPIC_AUTH_TOKEN in ~/.bashrc on $SERVER..." # Use bash -s + heredoc to safely inject the dynamic token line without quoting nightmares. # The token is sourced from the credentials file at each login — no literal token in .bashrc. -ssh "$SERVER" 'bash -s' <<'REMOTESCRIPT' +ssh -o ConnectTimeout=10 "$SERVER" 'bash -s' <<'REMOTESCRIPT' touch ~/.bashrc -sed -i '/ANTHROPIC_AUTH_TOKEN/d' ~/.bashrc +sed -i '/^export ANTHROPIC_AUTH_TOKEN=/d' ~/.bashrc cat >> ~/.bashrc <<'BASHLINE' export ANTHROPIC_AUTH_TOKEN=$(python3 -c "import json,os; d=json.load(open(os.path.expanduser('~/.claude/.credentials.json'))); print(d['claudeAiOauth']['accessToken'])" 2>/dev/null) BASHLINE @@ -93,7 +92,7 @@ ok "ANTHROPIC_AUTH_TOKEN set in ~/.bashrc (reads from credentials file)" # hasCompletedOnboarding is absent from the config, regardless of auth state. info "Marking onboarding complete on $SERVER..." -ssh "$SERVER" "python3 - <<'PYEOF' +ssh -o ConnectTimeout=10 "$SERVER" "python3 - <<'PYEOF' import json, os, subprocess path = os.path.expanduser('~/.claude.json') @@ -118,7 +117,7 @@ ok "~/.claude.json updated" # --- 6. Verify --- info "Verifying auth on $SERVER..." -RESULT=$(ssh "$SERVER" "claude auth status 2>/dev/null" 2>/dev/null || true) +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 From 7fa5fa7628c96cab9be7a5638ce3933e3dd6f4be Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 12:14:53 +0200 Subject: [PATCH 06/12] chore: bump version and changelog (v0.0.1.0) Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ VERSION | 1 + 2 files changed, 25 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a84cd0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [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`, injects `ANTHROPIC_AUTH_TOKEN` into `~/.bashrc` (read dynamically from the credentials file at login, not stored as plaintext), 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/VERSION b/VERSION new file mode 100644 index 0000000..0866607 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1.0 From 9e634b4fec3ba8b72c4a2dc77cd37506ab56e937 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 12:16:39 +0200 Subject: [PATCH 07/12] docs: sync documentation with shipped changes --- README.md | 1 + 1 file changed, 1 insertion(+) 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 From 8968d20819d1db0c10a3434999d58ab4285bcec8 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 16:05:29 +0200 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20remove=20ANTHROPIC=5FAUTH=5FTOKEN?= =?UTF-8?q?=20from=20auth=20setup=20=E2=80=94=20OAuth=20env=20var=20causes?= =?UTF-8?q?=20401?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting ANTHROPIC_AUTH_TOKEN to an OAuth token (sk-ant-oat01-...) causes "OAuth authentication is currently not supported" errors. Claude Code reads ~/.claude/.credentials.json natively for Max/Pro plan auth. The env var is only for API keys (ANTHROPIC_API_KEY). Script now removes any stale ANTHROPIC_AUTH_TOKEN from ~/.bashrc instead of injecting a new one. Verified working on dev server. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- server/README.md | 2 +- server/setup-claude-auth.sh | 26 +++++++++----------------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a84cd0..4d1e646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### 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`, injects `ANTHROPIC_AUTH_TOKEN` into `~/.bashrc` (read dynamically from the credentials file at login, not stored as plaintext), and marks onboarding complete in `~/.claude.json` so the theme picker never blocks startup. Replaces a painful multi-step manual process. +- `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 diff --git a/server/README.md b/server/README.md index dde8d12..e255cac 100644 --- a/server/README.md +++ b/server/README.md @@ -40,7 +40,7 @@ The fix: push credentials from your Mac directly. ./setup-claude-auth.sh dev # replace "dev" with your SSH alias ``` -This copies your OAuth token from the macOS Keychain to the server, sets `ANTHROPIC_AUTH_TOKEN` in `~/.bashrc`, and marks onboarding complete in `~/.claude.json`. After that, `ssh dev && claude` works with no prompts. +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`. diff --git a/server/setup-claude-auth.sh b/server/setup-claude-auth.sh index f961217..9e1648c 100755 --- a/server/setup-claude-auth.sh +++ b/server/setup-claude-auth.sh @@ -10,7 +10,7 @@ # What it does: # 1. Extracts your OAuth credentials from the macOS Keychain # 2. Copies them to the server as ~/.claude/.credentials.json -# 3. Sets ANTHROPIC_AUTH_TOKEN in ~/.bashrc (reads from credentials file at login) +# 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 @@ -70,22 +70,14 @@ 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. Set ANTHROPIC_AUTH_TOKEN in .bashrc --- -# Read the token dynamically from the credentials file at login rather than storing -# the literal token value. This keeps ~/.bashrc free of credential plaintext and -# automatically picks up refreshed tokens without re-running this script. - -info "Setting ANTHROPIC_AUTH_TOKEN in ~/.bashrc on $SERVER..." -# Use bash -s + heredoc to safely inject the dynamic token line without quoting nightmares. -# The token is sourced from the credentials file at each login — no literal token in .bashrc. -ssh -o ConnectTimeout=10 "$SERVER" 'bash -s' <<'REMOTESCRIPT' -touch ~/.bashrc -sed -i '/^export ANTHROPIC_AUTH_TOKEN=/d' ~/.bashrc -cat >> ~/.bashrc <<'BASHLINE' -export ANTHROPIC_AUTH_TOKEN=$(python3 -c "import json,os; d=json.load(open(os.path.expanduser('~/.claude/.credentials.json'))); print(d['claudeAiOauth']['accessToken'])" 2>/dev/null) -BASHLINE -REMOTESCRIPT -ok "ANTHROPIC_AUTH_TOKEN set in ~/.bashrc (reads from credentials file)" +# --- 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 From 5e096eaea83a75947dc8e1b8ac4b8b70d2ba4f3b Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 16:28:44 +0200 Subject: [PATCH 09/12] feat: add mac/zshrc with dev server iTerm2 connection helpers dev() opens one iTerm2 tab per tmux session on the dev server. devs lists running sessions without attaching. Co-Authored-By: Claude Sonnet 4.6 --- mac/zshrc | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 mac/zshrc 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'" From 8f8be2bb7205759713525d53e90253ea64f50acd Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 17:33:41 +0200 Subject: [PATCH 10/12] feat: add boot script for auto-starting tmux+claude sessions per repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server/terminal/start-claude-sessions.sh: on @reboot, finds every git repo under /root (maxdepth 2) and starts a named tmux session for each, cding into the repo and running "happy claude". Idempotent. - server/terminal/README.md: documents Happy (mobile/web Claude access), the boot script, and install steps - server/README.md: Quick Deploy steps 6 (auto-start) and 7 (Happy) - VERSION: 0.0.1.0 → 0.0.2.0 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 9 +++++ VERSION | 2 +- server/README.md | 24 +++++++++++++ server/terminal/README.md | 46 ++++++++++++++++++++++++ server/terminal/start-claude-sessions.sh | 34 ++++++++++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 server/terminal/start-claude-sessions.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1e646..5ab4903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 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 diff --git a/VERSION b/VERSION index 0866607..b456a71 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1.0 +0.0.2.0 diff --git a/server/README.md b/server/README.md index e255cac..30b042c 100644 --- a/server/README.md +++ b/server/README.md @@ -86,6 +86,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/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..cff8d2b --- /dev/null +++ b/server/terminal/start-claude-sessions.sh @@ -0,0 +1,34 @@ +#!/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" + tmux send-keys -t "$name" "happy claude" Enter + done From 078ac5f2c9c3a7a9d3034ea01c5c3005c6a15a01 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Tue, 14 Apr 2026 18:26:04 +0200 Subject: [PATCH 11/12] fix: pass happy claude as session command instead of send-keys tmux.conf sets default-command, so send-keys was typing into the already-running Claude Code session. Pass the command directly to tmux new-session instead. Co-Authored-By: Claude Opus 4.6 --- server/terminal/start-claude-sessions.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/terminal/start-claude-sessions.sh b/server/terminal/start-claude-sessions.sh index cff8d2b..da220e2 100644 --- a/server/terminal/start-claude-sessions.sh +++ b/server/terminal/start-claude-sessions.sh @@ -29,6 +29,5 @@ find /root -maxdepth 2 -name ".git" -type d 2>/dev/null \ if tmux has-session -t "$name" 2>/dev/null; then continue fi - tmux new-session -d -s "$name" -c "$repo" - tmux send-keys -t "$name" "happy claude" Enter + tmux new-session -d -s "$name" -c "$repo" "happy claude" done From a90a85b11bad74bf20f7e6f45a402f7a9bfe6d18 Mon Sep 17 00:00:00 2001 From: Paul Schraven Date: Wed, 15 Apr 2026 08:38:47 +0200 Subject: [PATCH 12/12] docs: add workflow overview to server README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the end-to-end pattern: Mac iTerm2 → dev server tmux sessions → happy claude for mobile access, with day-to-day commands and re-auth note. Co-Authored-By: Claude Opus 4.6 --- server/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/server/README.md b/server/README.md index 30b042c..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+