* 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>
echo "$response" | grep -q can cause "write error: Broken pipe" when
grep -q exits early and echo gets SIGPIPE. This is non-deterministic
and depends on response size and timing, which is why it only fails
intermittently in CI. Using [[ == *pattern* ]] avoids pipes entirely.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After an interactive SSH session ends, users are now shown:
- A warning that their server is still running (and may incur charges)
- A link to the cloud provider's dashboard to manage/delete it
- The SSH command to reconnect
This prevents users from unknowingly leaving servers running after
exiting their agent session. Covers all 25 SSH-based cloud providers.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unquoted `<< EOF` heredocs in nanoclaw .env file creation cause shell
expansion of the API key value. If an API key contains `$`, backticks,
or `\`, the value is silently corrupted or could trigger command
execution. Replace with `printf '%s'` which safely writes the value
without interpretation.
Also fix unquoted variable expansion in upload_config_file's mv command
and the github-codespaces/openclaw.sh config heredoc.
Fixes 34 scripts across all cloud providers.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add log_install_failed helper to shared/common.sh that provides
structured troubleshooting for agent install failures: possible causes,
SSH debug command (when server IP available), manual install command,
and re-run suggestion. Also improve SSH key registration error message.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SSH key registration in 11 cloud providers used unescaped key_name
directly in JSON request bodies. If the hostname (used to generate
key names) contained JSON-special characters like double-quotes, it
could break out of the JSON string and inject arbitrary JSON fields.
Fix: use json_escape for key_name in all providers, matching the
pattern already used by Scaleway.
Also fix GCP create_server which embedded the startup script inline
in --metadata with comma delimiters. Commas in the script could break
metadata parsing or inject additional metadata keys. Fix: use
--metadata-from-file for the startup script.
Affected providers: Hetzner, DigitalOcean, Vultr, BinaryLane,
Hostinger, Contabo, Cherry, HOSTKEY, Civo, Linode, Genesis Cloud, GCP.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract `ensure_jq()` from hetzner and hostkey into shared/common.sh,
eliminating 64 lines of identical duplicated code
- Decompose DigitalOcean `create_server()` by extracting error handling
into `_do_check_create_error()` helper, and using the shared
`extract_api_error_message` instead of inline Python parsing
- Use shared `_extract_json_field` for droplet ID extraction
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace technical "Remediation steps:" with "How to fix:" and
"Remediation: Check <url>" with "Check your dashboard: <url>" across
14 cloud providers for clearer error guidance. Add actionable error
messages to Atlantic.Net create_server and SSH key registration failures.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Hetzner lib: replace all Python JSON parsing with jq. Uses the
/datacenters API as the authoritative source for server type
availability (server_types.available), cross-referenced with
/server_types for specs and pricing. jq is auto-installed if missing.
URLs: update openrouter.ai/lab/spawn → openrouter.ai/labs/spawn
across all READMEs and CLI source.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 10 inline `python3 -c "import json,sys; d=json.loads(...)..."` one-liners
across vultr, hetzner, digitalocean, and contabo with calls to a new shared
`extract_api_error_message` helper in shared/common.sh. The helper tries common
JSON error field patterns (message, error, error.message, error.error_message,
reason) and falls back to a caller-specified default.
This pattern appears 35+ times across cloud libs; this PR converts the first 4
clouds as a proof of concept. Remaining clouds can adopt incrementally.
Net reduction: 10 lines per converted cloud (~3 lines saved per call site).
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 cloud providers had identical 5-line check_ssh_key functions that
fetch SSH keys from the provider API and grep for the fingerprint.
Extract this pattern into a shared check_ssh_key_by_fingerprint helper
in shared/common.sh, reducing each cloud's function to a single line.
Affected clouds: BinaryLane, Cherry, Civo, Contabo, DigitalOcean,
Genesis Cloud, Hetzner, Hostinger, Latitude, Linode, OVH, Scaleway,
Vultr.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
18 cloud lib/common.sh files had identical 7-line get_server_name()
functions (get_resource_name + validate_server_name + echo). Added a
shared get_validated_server_name helper to shared/common.sh and replaced
all duplicates with one-line delegations. Net -110 lines.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
~1500 progress messages across 481 files were using log_warn (yellow)
for normal status updates like "Installing...", "Setting up...",
"Creating server...", etc. This made users think something was wrong
when everything was proceeding normally.
Changes:
- Replace log_warn with log_step for all progress/status messages
- Keep log_warn only for actual warnings (errors, remediation hints)
- Remove emoji from 3 sprite completion messages
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
- Add log_step() function (cyan) for status/progress messages
- Convert misused log_warn calls to log_step in shared/common.sh
(14 instances: SSH key gen, agent verification, waiting, configuring)
- Convert representative cloud scripts: hetzner, digitalocean, sprite
- Fix misleading validatePrompt error that suggested --prompt-file as a
workaround when it has the same validation
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ssh_run_server, ssh_upload_file, ssh_interactive_session, and
ssh_verify_connectivity to shared/common.sh. These four functions
were copy-pasted identically across 21 cloud provider lib files,
differing only in SSH username (root vs ubuntu).
Providers now set SSH_USER and delegate to the shared helpers via
one-line wrappers, reducing each provider's lib by ~20 lines.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Seven cloud providers had nearly identical instance status polling loops
(20-36 lines each). Extract the shared pattern into generic_wait_for_instance()
in shared/common.sh and replace the duplicated loops with one-liner calls.
Clouds refactored: Civo, Contabo, DigitalOcean, GenesisCloud, Linode, UpCloud, Vultr
Net reduction: ~99 lines (-185/+86)
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Replace vulnerable heredoc patterns across 27 continue.sh scripts with
setup_continue_config() helper that uses json_escape() + upload_config_file()
to safely handle API keys containing special characters like quotes or braces.
Also fix _save_token_to_config() in shared/common.sh which had the same
unescaped heredoc vulnerability for local token storage.
Relates to #104
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix triple-quote injection in SSH keys (Scaleway, UpCloud), userdata
(BinaryLane), init scripts (Civo, Kamatera), and GraphQL queries
(RunPod) by passing data via stdin/json_escape instead of inline
string interpolation
- Add input validation for all cloud provider env vars (region, type,
plan, etc.) using validate_region_name/validate_resource_name to block
shell metacharacters before they reach Python string interpolation
- Validate Modal image name as Python identifier to prevent code injection
- Validate numeric env vars (RAM, GPU count, disk size) across all providers
Affects: 19 cloud provider lib/common.sh files
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show clear error when --prompt/-p or --prompt-file is used without a
value (previously silently ignored)
- Fix --prompt-file splice index bug when used after --prompt
- Replace echo -e with printf in fly/lib/common.sh for macOS bash 3.x
compatibility
- Fix incorrect env var name in README (DIGITALOCEAN_TOKEN -> DO_API_TOKEN)
- Add missing agent entries (gptme, OpenCode, Plandex) to 11 cloud READMEs
- Add all 13 agents to Civo README (previously only had 3)
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upstream OpenCode installer pipes `curl -# -L | tar xz` which fails
in container exec environments (Sprite, E2B, Modal, Daytona) where the
binary stream gets corrupted through the exec layer, producing
"gzip: stdin: not in gzip format" errors.
Added opencode_install_cmd() to shared/common.sh that downloads the
binary to a file first, then extracts it. Updated all 17 opencode.sh
scripts to use this robust method instead of the upstream installer.
The previous fix (#44) only addressed Sprite with a hardcoded
linux-x86_64 architecture. This fix detects OS/arch dynamically and
applies to all cloud providers.
Fixes#42
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plandex is an open source AI coding agent for complex tasks (15k+ GitHub
stars, multiple HN frontpage posts). It natively supports OpenRouter via
OPENROUTER_API_KEY environment variable and installs via a single curl
command. Go-based CLI with sandbox and version control for AI changes.
Implemented on all 14 clouds: sprite, hetzner, digitalocean, vultr,
linode, lambda, aws-lightsail, gcp, e2b, modal, fly, civo, scaleway,
daytona.
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add gptme agent to spawn matrix
Add gptme (https://github.com/gptme/gptme) - a personal AI agent in the
terminal with tools for code editing, terminal commands, web browsing,
and more. Natively supports OpenRouter via OPENROUTER_API_KEY.
- Add gptme agent entry to manifest.json with OpenRouter env vars
- Implement sprite/gptme.sh deployment script
- Implement hetzner/gptme.sh deployment script
- Add "missing" matrix entries for remaining 8 clouds
- Update README.md with usage instructions for Sprite and Hetzner
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Fly.io cloud provider with claude and aider agents
Add Fly.io as a new cloud provider using the Machines REST API for
provisioning and flyctl CLI for SSH access. Docker-based machines
with pay-per-second pricing.
- Create fly/lib/common.sh with Fly.io Machines API integration
- Implement fly/claude.sh for Claude Code deployment
- Implement fly/aider.sh for Aider deployment
- Update README.md with Fly.io usage instructions and env vars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add gemini, amazonq, cline, gptme to Fly.io
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add openclaw, nanoclaw, goose, codex, interpreter to Fly.io
Implements 5 new agent scripts for the Fly.io cloud provider:
- fly/openclaw.sh: OpenClaw with gateway + TUI, model selection, config
- fly/nanoclaw.sh: NanoClaw WhatsApp agent with .env configuration
- fly/goose.sh: Block's Goose agent with OpenRouter provider
- fly/codex.sh: OpenAI Codex CLI with OpenRouter base URL override
- fly/interpreter.sh: Open Interpreter with OpenRouter base URL override
All scripts follow the Fly.io pattern (flyctl-based, no IP args for
run_server/interactive_session) and use upload_file for env injection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add gptme agent to 8 remaining clouds
Implement gptme agent scripts for digitalocean, vultr, linode, lambda,
aws-lightsail, gcp, e2b, and modal. Each script follows the exact
pattern of that cloud's existing aider.sh, adapted for gptme's install
and launch commands. Updates manifest.json matrix entries from "missing"
to "implemented".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add guardrails from insights: CLAUDE.md rules, hooks, pre-commit
Based on usage insights analysis:
CLAUDE.md:
- Shell script rules: curl|bash compat, macOS bash 3.x compat
- Autonomous loop rules: test after each iteration, never revert fixes
- Git workflow rules: always use feature branches
.claude/settings.json:
- PostToolUse hook validates .sh files on every Write/Edit:
syntax check, no relative source, no echo -e, no set -u
.githooks/pre-commit:
- Blocks commits with: syntax errors, relative sources, echo -e,
set -euo, references to deleted functions
- Install: git config core.hooksPath .githooks
README.md:
- Added developer setup section with hook installation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed SC2086 warnings by adding quotes around ${INSTANCE_STATUS_POLL_DELAY}
in 4 provider libraries. This prevents potential word splitting bugs if
the variable contains unexpected whitespace.
Files updated:
- linode/lib/common.sh:231
- vultr/lib/common.sh:226
- aws-lightsail/lib/common.sh:133
- digitalocean/lib/common.sh:211
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- hetzner/lib/common.sh:31 - HCLOUD_TOKEN set by caller
- digitalocean/lib/common.sh:36 - DO_API_TOKEN set by caller
- Silences false positive warnings for intentionally external variables
- These tokens are exported by ensure_*_token functions before use
- 5 nanoclaw scripts only create DOTENV_TEMP but trap referenced both
- Causes trap to fail silently when trying to rm undefined variable
- Fix: Change trap from both temps to just DOTENV_TEMP
- Affected: digitalocean, hetzner, linode, sprite, vultr
Root cause: Automated trap addition in Round 19 incorrectly detected
these files as using both temp variables (DOTENV_TEMP contains substring
ENV_TEMP which caused false positive in grep check)
- Add trap 'rm -f "${ENV_TEMP}"' EXIT after mktemp creation
- Scripts with DOTENV_TEMP get combined trap for both files
- Remove manual rm calls that are now redundant
- Prevents temp file leaks on early script exit (errors, signals)
- Affects 67 agent scripts across all providers
Impact: Prevents /tmp pollution in production deployments
Score: 90 (Impact: 9, Confidence: 10, Risk: 1)
Fixed 2 critical syntax errors in API error handling:
- Hetzner line 160-161: Malformed error_msg assignment
- DigitalOcean line 172-173: Broken error_msg assignment
These were introduced during Round 15 refactoring and would crash
whenever users encountered API errors during server creation.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add SC2086 disable comments to interactive_session() functions in
GCP, Hetzner, and DigitalOcean providers. SSH_OPTS is intentionally
unquoted to allow word splitting for multiple SSH options.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add explicit timeout parameter (60 seconds) to all wait_for_cloud_init calls
across linode, vultr, digitalocean, hetzner, gcp, and aws-lightsail providers.
This makes timeout configuration more explicit and easier to tune per-provider
or per-agent in the future. The value of 60 matches the default in the
wait_for_cloud_init function implementations.
Modified 60 agent scripts (10 agents × 6 providers).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add shellcheck source comments to all agent scripts
- Tells shellcheck where provider-exported variables are defined
- Fix 132+ SC2154 warnings across all providers
Score: 30 (Impact: 6, Confidence: 10, Risk: 2)
Eliminates duplicate SSH key registration logic across 5 cloud providers
(Hetzner, DigitalOcean, Vultr, Linode, Lambda) by introducing a generic
callback-based pattern in shared/common.sh.
Before: Each provider had ~45 lines of nearly identical code for:
- Generating SSH keys if missing
- Getting fingerprints
- Checking if key exists with provider
- Registering key if not exists
- Error handling
After: Providers implement 2 simple callbacks:
- check_callback: provider-specific API call to check if key exists
- register_callback: provider-specific API call to register key
The shared function handles:
- Key generation (via generate_ssh_key_if_missing)
- Fingerprint extraction (via get_ssh_fingerprint)
- Flow control and logging
- Callback orchestration
Changes:
- shared/common.sh: Added ensure_ssh_key_with_provider() function
- hetzner/lib/common.sh: Refactored to use callbacks
- digitalocean/lib/common.sh: Refactored to use callbacks
- vultr/lib/common.sh: Refactored to use callbacks
- linode/lib/common.sh: Refactored to use callbacks
- lambda/lib/common.sh: Refactored to use callbacks
Benefits:
- DRY: Eliminates ~220 lines of duplicate code
- Maintainability: Bug fixes in registration flow benefit all providers
- Consistency: All providers use identical registration logic
- Extensibility: New providers can reuse this pattern
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
SSH_OPTS contains multiple flags that must be word-split, so unquoted
usage is intentional. Added shellcheck directives to suppress false
positive warnings across all cloud provider common.sh files.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>