Users on exec-based clouds (Fly, Render, Koyeb, Northflank, Railway,
Modal, Daytona, E2B, CodeSandbox, GitHub Codespaces) got no warning
when their session ended that their service was still running and
incurring charges. This adds:
- _show_exec_post_session_summary() in shared/common.sh for non-SSH
providers that use CLI exec commands instead of direct SSH
- SPAWN_DASHBOARD_URL for all 10 exec-based clouds so users get
actionable dashboard links
- Post-session summary calls in each cloud's interactive_session()
- 33 new tests covering the exec post-session summary feature
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>
- fly/lib/common.sh: Replace 23-line get_server_name() that duplicated
env-var-check, prompt, and validation logic with a one-line call to the
shared get_validated_server_name helper, matching all other cloud providers.
- oracle/lib/common.sh: Break _setup_vcn_networking (48 lines, 3 distinct
responsibilities) into focused helpers:
- _create_internet_gateway: creates the IGW resource
- _add_default_route: configures the route table
- _add_ssh_security_rules: opens SSH port in the security list
The orchestrator _setup_vcn_networking now delegates to these three helpers.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Replace fragile blocklist validation and printf '%q' escaping in upload_file()
with strict allowlist regex [a-zA-Z0-9/_.~-]+ across all non-SSH cloud providers.
For codesandbox, additionally migrate from shell command interpolation to SDK
filesystem API via environment variables, eliminating the injection surface entirely.
Affected clouds: codesandbox, daytona, e2b, fly, koyeb, modal, northflank,
railway, render, sprite
Fixes#989
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>
Improve error messages in cloud provider lib/common.sh files to include
specific troubleshooting steps, dashboard URLs, and environment variable
hints instead of bare "Failed" messages.
Providers improved: Netcup, IONOS, CloudSigma, Northflank, UpCloud,
Fly.io, RamNode, OVH, Civo, Scaleway.
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>
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 40 lines of duplicated inline Claude Code configuration in each
of fly/claude.sh, koyeb/claude.sh, and railway/claude.sh with the shared
setup_claude_code_config helper from shared/common.sh. This eliminates
~120 lines of copy-pasted config generation (mktemp, heredoc, upload, rm)
and uses json_escape for the API key, matching the pattern already used
by 30+ other claude.sh scripts.
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 25-line custom _binarylane_wait_for_active with 4-line
generic_wait_for_instance call, matching the pattern used by 7 other
clouds (DigitalOcean, Vultr, Linode, etc).
Change log_warn to log_step for status/progress messages in polling
loops across 7 cloud providers (aws-lightsail, exoscale, fly, kamatera,
latitude, ovh, scaleway). These are normal status updates, not warnings.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <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 printf %q command escaping to run_server/interactive_session in
Koyeb, Render, Railway, and GitHub Codespaces (matching pattern used
by E2B, Daytona, Northflank, Fly, and other providers)
- Use json_escape in exchange_oauth_code to prevent JSON injection via
crafted OAuth codes in shared/common.sh
- Use json_escape in Fly.io _fly_create_app to prevent JSON injection
via FLY_ORG env var, plus add validation for org slug format
- Pass Fly.io _fly_create_machine values via env vars instead of Python
string interpolation to prevent code injection
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace unsafe printf '%q'-escaped unquoted variables with validated
single-quoted embedding in upload_file() for fly, northflank, daytona,
e2b, and koyeb. The previous pattern used unquoted $escaped_content and
$escaped_path in command strings passed to bash -c or run_server, which
could allow command injection via crafted filenames.
The fix:
- Validates remote_path rejects unsafe chars (', $, `, newlines)
- Uses base64 content directly (alphanumeric + /+= is shell-safe)
- Single-quotes both content and path in the command string
- Uses printf '%s' instead of echo for safer output
Matches the pattern already used by render, modal, and railway.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 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>
Adds Continue CLI support on Fly.io by combining Fly.io's Machines API
primitives with Continue's setup steps. Includes OpenRouter config injection
via ~/.continue/config.json.
Agent: gap-filler-fly-continue
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Split _fly_create_and_start_machine (70 lines) into _fly_create_machine
and _fly_wait_for_machine_start for single-responsibility
- Replace ensure_koyeb_token (38 lines) with ensure_api_token_with_provider
- Replace ensure_railway_token (37 lines) with ensure_api_token_with_provider
- Remove _save_koyeb_token and _save_railway_token (handled by shared helper)
Net reduction: ~80 lines of duplicated code
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>
json_escape() returns a fully-quoted JSON string (e.g. "value") via
Python's json.dumps(). Callers using printf templates were wrapping
the result in additional quotes ("%s"), producing invalid JSON like
""value"". Remove the redundant quotes from all printf format strings
so json_escape's quotes are used directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Break down the two longest create_server functions (104 and 102 lines)
into focused sub-functions for readability and reusability:
Scaleway (104 -> 53 lines):
- Extract _scaleway_extract_ip() for IP parsing from server response
- Extract _scaleway_power_on_and_wait() for power-on + polling loop
Fly.io (102 -> 14 lines):
- Extract _fly_create_app() for app creation with "already exists" handling
- Extract _fly_create_and_start_machine() for machine lifecycle
Also fix ((attempt++)) to attempt=$((attempt + 1)) in Fly.io to avoid
potential set -e failures when attempt is 0.
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>
* refactor: Extract duplicated prompt flag parsing into extractFlagValue helper
The --prompt and --prompt-file argument extraction in main() shared identical
patterns for flag detection, value validation, and args splicing. Extracted
into a reusable extractFlagValue() function that handles all three concerns.
Agent: complexity-hunter
* refactor: Consolidate multiple python3 JSON reads into single calls
OVH, Kamatera, and UpCloud each spawned separate python3 processes to
read different fields from the same JSON config file. Consolidate into
a single python3 call per file, printing all fields at once and reading
them with bash read. Also fixes OVH using string interpolation for the
file path instead of the safer sys.argv[1] pattern.
Agent: complexity-hunter
* refactor: Extract flyctl auth and token validation from ensure_fly_token
Split the 75-line ensure_fly_token into focused helpers:
- _try_flyctl_auth: encapsulates flyctl CLI token retrieval
- _validate_fly_token: encapsulates API validation with error reporting
The main function is now a clear sequential flow of token source attempts.
Agent: complexity-hunter
* refactor: Deduplicate retry backoff logic in kamatera_api
The two error branches (network error and HTTP 429/503) had identical
interval update and attempt increment code. Restructure with early
return for success, then unified backoff at the end of the loop.
Agent: complexity-hunter
* refactor: Remove unnecessary async IIFE wrapper in validateAndGetAgent
The function wrapped its body in `return (async () => { ... })()` when
it can simply be declared as `async function` directly.
Agent: complexity-hunter
---------
Co-authored-by: A <6723574+louisgv@users.noreply.github.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>
Security:
- Fix command injection in modal/lib/common.sh (run_server, upload_file, interactive_session)
- Fix command injection in fly/lib/common.sh (run_server, upload_file, interactive_session)
- All container providers now use printf '%q' for proper shell escaping
Complexity:
- Extract _api_should_retry_on_error() helper in shared/common.sh (-19 lines)
- Refactor scaleway_api and upcloud_api to use shared retry helper (-24 lines)
- Extract _save_fly_token() helper in fly/lib/common.sh (-11 lines)
- Extract validateAndGetAgent() in commands.ts, reducing cmdRun/cmdAgentInfo duplication
- Refactor cmdList column width calculation to use calculateColumnWidth()
UX:
- Add actionable next steps to error messages in shared/common.sh
- Improve CLI bash fallback error messages with guidance (spawn.sh)
- Add OAuth progress indicator during browser authentication wait
- Show invalid model ID value and link to openrouter.ai/models
- Add troubleshooting steps for agent installation failures
Tests:
- Update test assertions in test/run.sh to match refactored patterns
- All tests passing: 74 TypeScript + 75 bash = 149 total, 0 failures
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>
Completed ENV_TEMP pattern extraction across remaining providers:
1. Modal: gptme.sh (1 script) - uses inject_env_vars_local
2. GCP: all 10 agent scripts - uses inject_env_vars_ssh
3. Fly.io: all 11 agent scripts - uses new inject_env_vars_fly
- Added inject_env_vars_fly() to fly/lib/common.sh
- Handles both .bashrc and .zshrc (Fly-specific requirement)
4. Sprite: amazonq, cline, gemini (3 scripts) - uses inject_env_vars_sprite
Total scripts converted in this commit: 25
Total scripts converted in Round 25 Task #1: 78 scripts
Each conversion replaces 11-15 lines of temp file management with a single
function call that handles creation, permissions, content generation, upload,
sourcing, and cleanup.
The only remaining ENV_TEMP patterns are DOTENV_TEMP in nanoclaw scripts,
which are agent-specific .env files and should remain as-is.
Co-Authored-By: Claude Sonnet 4.5 <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>