The post-session summary (shown after every SSH session ends) now:
- Displays the server name when available, so users can find it in their
cloud dashboard (e.g., "Your server 'spawn-claude-abc' is still running")
- Adds explicit billing reminder ("Remember to delete it to avoid charges")
- Uses green (log_info) for reconnect instructions instead of yellow
(log_warn), since reconnect info is helpful guidance, not a warning
No changes to individual cloud scripts needed -- all scripts already set
SERVER_NAME before calling interactive_session.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <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 regex validation (^[a-z0-9][a-z0-9._-]{0,63}$) to invalidate_cloud_key()
in shared/key-request.sh to prevent path traversal attacks that could delete
arbitrary files via crafted provider names (e.g., ../../etc/important)
- Improve validKeyVal() in key-server.ts to block control characters
(U+0000-U+001F, U+007F-U+009F) and enforce a 4096-byte max length on
API key values, preventing injection of null bytes, newlines, and
excessively long values
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 three helpers from the 82-line, 14-conditional function:
- _parse_cloud_auths: extract cloud auth specs from manifest.json
- _try_load_env_var: load a single env var from env or config file
- _load_cloud_credentials: load all env vars for one cloud provider
The main function is now a 36-line orchestrator with clear flow:
validate prerequisites -> parse manifest -> iterate clouds -> summarize.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Extract platform-specific install logic from monolithic installer functions
into small, focused helpers. Both functions had nested OS/package-manager
cascades (depth 3-4) that made the control flow hard to follow.
ensure_jq (shared/common.sh):
- Extract _install_jq_brew, _install_jq_apt, _install_jq_dnf, _install_jq_apk
- Extract _report_jq_not_found for the fallthrough error message
- Main function becomes a clean dispatcher + verification
ensure_gh_cli + _install_gh_binary (shared/github-auth.sh):
- Extract _install_gh_brew, _install_gh_apt, _install_gh_dnf
- Extract _detect_gh_platform, _fetch_gh_latest_version, _download_and_install_gh
- _install_gh_binary drops from 71 to 12 lines as a clean orchestrator
- ensure_gh_cli drops from 57 to 29 lines
No behavior changes. All tests pass, bash -n passes.
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>
Improve error messages in shared utilities and cloud providers that
previously showed bare "Failed to..." messages without telling users
how to fix the problem.
Shared (shared/common.sh):
- generate_ssh_key_if_missing: handle ssh-keygen/mkdir failures with
disk space and permission guidance
- get_ssh_fingerprint: detect missing/corrupt public key files with
regeneration instructions
- generic_ssh_wait: structured "How to fix" with manual SSH test command
and firewall check
- _report_api_failure: add DNS/firewall/proxy guidance for network errors
- ensure_jq: platform-specific install commands for unknown package
managers, hash rehash hint after install
- get_openrouter_api_key_manual: structured guidance after 3 failed
attempts
Cloud providers:
- Contabo: actionable guidance for OAuth token failures
- Exoscale: guidance for credential validation and CLI download failures
- Netcup: network connectivity hint for API connection failure
- Scaleway: structured guidance for project ID lookup failure
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <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>
- Remove duplicate ensure_jq() function in shared/common.sh (lines 2341-2372)
that was accidentally left after extracting it to the shared lib in #946
- Move "Aliases: ls, history" onto the "spawn list" help line so it no longer
appears to describe "spawn list --clear"
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>
- 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>
- Remove redundant "Warning:" prefix from API key format message (log_warn
already conveys warning status)
- Fix incorrect `export VAR=token spawn ...` syntax in auth failure hint
(export makes it persistent, inline env var syntax is correct)
- Replace attempt/retry jargon with elapsed time in SSH wait and instance
polling messages (users care about time, not internal retry counts)
- Show instance IP in friendlier "ready (IP: x.x.x.x)" format
- Move HTTP status codes from error title to body in download failures
(cleaner error headline, details still available)
- Simplify dry-run credential warning (remove confusing double-negative
"without --dry-run")
- Remove redundant "Warning:" prefix from extra arguments message
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
- Change SSH default from StrictHostKeyChecking=no to accept-new, which
accepts host keys on first connection but rejects if they change later
(Trust On First Use). This protects against MITM attacks on subsequent
connections. Requires OpenSSH 7.6+ (released Oct 2017).
- Replace predictable $$-based temp file path in upload_config_file with
$RANDOM to prevent symlink attacks on the remote server.
Addresses findings from issue #763.
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>
Reduce cyclomatic complexity in the two highest-scoring functions:
- cli/src/commands.ts: Extract `handleUserInterrupt` and `runWithRetries`
from `execScript` (complexity score 6 -> 2 for execScript, retry logic
now independently testable)
- shared/common.sh: Extract `_classify_api_result` and `_report_api_failure`
from `_cloud_api_retry_loop` (complexity score 9 -> 4, removes duplicated
error-classification logic from loop body)
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>
Standalone, sourceable script that installs the gh CLI and runs
interactive gh auth login. Any agent script on any cloud can source
it and call ensure_github_auth to get authenticated with GitHub.
- ensure_gh_cli: installs via brew/apt/dnf/binary fallback
- ensure_gh_auth: uses GITHUB_TOKEN or interactive OAuth flow
- ensure_github_auth: combined convenience wrapper
- Idempotent, macOS bash 3.x compatible, curl|bash compatible
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All spawn environments are disposable cloud VMs. Setting IS_SANDBOX=1
helps agents like Claude Code recognize the environment as a sandbox,
avoiding unnecessary safety prompts for root-level operations.
Added in two places for full coverage:
- generate_env_config(): included automatically in every env injection
- get_cloud_init_userdata(): set in .bashrc/.zshrc during cloud-init
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>
In-progress actions (installing, starting, connecting...) should use
log_step (cyan) to visually distinguish them from completion messages
which use log_info (green). This makes it easier for users to see at a
glance what is happening vs what has finished.
Changes:
- cli/install.sh: add log_step function, use it for install progress
- shared/common.sh: OAuth flow and non-interactive exec messages
- Cloud libs: interactive_session, auth, and cleanup messages
- Agent scripts: gateway startup and session opening messages
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>
Consistently use log_step for progress/status messages ("Waiting for...",
"Fetching...", "Creating...") and reserve log_info for success/completion
messages. This gives users a clear visual distinction between operations
that are still running (cyan) vs operations that have completed (green).
Also adds periodic progress updates to silent polling loops in ramnode,
cherry, and netcup IP wait functions so users see activity during long waits.
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 hardcoded 4-cloud script list in run_shellcheck with dynamic
discovery that covers all 21 clouds automatically
- Convert 3 inline JSON templates (setup_claude_code_config,
setup_openclaw_config, setup_continue_config) from single-line printf
to readable heredocs while preserving json_escape security
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Consolidate duplicated curl logic from _make_api_request and
_make_api_request_custom_auth into a shared _curl_api core function,
reducing copy-paste and making both functions thin wrappers.
Extract inline Python JSON extraction from generic_wait_for_instance
into a reusable _extract_json_field helper.
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>
The `echo ""` on line 351 of get_model_id_interactive() was going to
stdout, causing it to be captured by command substitution into MODEL_ID.
This injected a newline into the openclaw.json config, breaking JSON
parsing with "invalid character '\n' at 15:0".
Fixes#553
Agent: issue-fixer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <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>
The generic_wait_for_instance timeout message previously just said
"did not become active in time" with no guidance. Now it follows the
same pattern as generic_ssh_wait by telling users what to do next.
Similarly, _validate_token_with_provider now shows the env var name
so users can set it directly instead of re-running interactively.
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 get_model_id_interactive function returned MODEL_ID from env vars
without calling validate_model_id, bypassing the allowlist check. Also
migrated 13 legacy scripts from raw safe_read to get_model_id_interactive
which includes validation.
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>
1. _cloud_api_retry_loop: consolidate two duplicate retry branches
(network error + HTTP 429/503) into a single retry path using a
retry_reason variable. Reduces from 47 to 43 lines, eliminates
duplicated _api_should_retry_on_error / _update_retry_interval calls.
2. interactive_pick: extract list display + selection into reusable
_display_and_select helper. The main function is now a thin wrapper
that checks env var, fetches items, then delegates to the helper.
3. generic_ssh_wait: replace inline backoff calculation (3 lines) with
existing _update_retry_interval helper, reducing duplication.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <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>
Remove 2 unnecessary indirection layers (_handle_api_transient_error and
_api_handle_transient_http_error) from the cloud API retry infrastructure.
The old _handle_api_transient_error had a bug where "network" was passed
as the attempt parameter to _api_should_retry_on_error, which expects a
numeric value. The retry logic is now inlined directly in
_cloud_api_retry_loop, calling _api_should_retry_on_error with the
correct arguments.
Also extract duplicated help-flag checking in dispatchCommand into a
hasTrailingHelpFlag helper, reducing nesting and removing repeated code.
Net: -72 lines, 2 fewer functions, 1 bug fix.
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>
Extract duplicated credential-hint logic from case 1/default into
credentialHint() helper, and flatten nested if-blocks in
generic_wait_for_instance using early-continue.
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>
Reduce complexity in the two most verbose functions in shared/common.sh:
- verify_agent_installed(): Extract repeated "Possible causes" / "How to fix"
error blocks into a reusable _log_diagnostic() helper, reducing 22 lines of
duplicated log_error calls to 2 structured calls.
- get_openrouter_api_key_oauth(): Flatten nested if/else by testing the
rejection case first (early return), eliminating the else branch and reducing
nesting depth.
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>
safe_read() now propagates read command failures instead of masking
them with the always-successful echo on the last line. Also adds a
3-attempt limit to get_openrouter_api_key_manual() as defense-in-depth.
Fixes#494
Agent: issue-fixer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add a generic ensure_multi_credentials() helper to shared/common.sh that
handles the env-var/config-file/prompt/test/save flow for providers needing
multiple credentials. This eliminates ~270 lines of duplicated logic across
contabo, netcup, ramnode, ionos, and upcloud, replacing it with single
function calls.
Each provider's ensure_*_credentials() function is now 3-8 lines instead
of 30-65 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>
- Clarify download error messages: distinguish HTTP errors from network errors
with specific status codes in the message
- Add actionable next steps to OAuth timeout: re-run command or set key manually
- Standardize error help labels to "How to fix:" across CLI and shell scripts
(was inconsistently "What to do:", "Troubleshooting:", or missing)
- Add API method/endpoint context to retry failure messages so users know
which API call failed
- Make verify_agent_installed error cases mutually exclusive: first for
PATH/installation issues, second for runtime/dependency issues
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>
- Cancel handling: use p.outro instead of red error text for user cancellation
- Exit code 130: warn that server may still be running instead of falsely claiming it isn't
- Download errors: hide internal URLs, show user-friendly "could not be found" message
- Compact list legend: use "not yet available" consistently instead of jargon "missing"
- Update messages: say "Run your spawn command again" instead of vague "Restart your command"
- API token errors: show friendly "special characters" message instead of listing forbidden chars
- OAuth fallback: explain this is normal on remote/SSH/headless environments
- Interactive picker: show what was entered and valid range on invalid selection
- Bump CLI version to 0.2.39
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.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>
Extract helpers from the two longest functions in shared code:
- try_oauth_flow() (60 -> 37 lines): Extract _init_oauth_session() for
temp dir + CSRF state setup, and _await_oauth_callback() for browser
open + timeout handling
- ensure_ovh_authenticated() (67 -> 28 lines): Extract _ovh_prompt_credentials()
for the interactive credential prompting, validation, and saving
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
- Add signal exit code handling (130/Ctrl+C, 137/killed, 255/SSH failure, 2/syntax error)
- Replace vague "Cloud API retry logic exhausted" with attempt count and retry advice
- Add network troubleshooting hint to API network error after retries
- Clarify OAuth fallback prompt: explain why OAuth failed and what happens next
- Consolidate auth cancellation message with three clear recovery options
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 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>
- Add "spawn update" hint to version output so users know how to update
- Simplify non-interactive TTY message (less alarming, more actionable)
- Fix _api_handle_transient_http_error passing wrong first arg to
_api_should_retry_on_error (was "http_429" instead of attempt number)
- Sync README matrix count (444 -> 445)
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>
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>
The generate_env_config function wrote `export KEY=VALUE` without quoting
the value. When these config files are sourced by the user's shell, any
shell metacharacters in values ($, `, \, spaces) would be interpreted,
potentially leading to arbitrary command execution.
Values are now single-quoted, which prevents all shell interpretation.
Single quotes within values are properly escaped using the standard
'\'' technique.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix auto-update unicode symbols (checkmark/cross) that bypassed unicode
detection, causing garbled output in SSH sessions and dumb terminals
- Use log_info (green) instead of log_warn (yellow) for OAuth progress
messages, so normal authentication flow doesn't look like a warning
- Add install path to `spawn version` output for easier debugging when
multiple versions are installed
- Improve --prompt-file errors to distinguish file-not-found, permission
denied, and is-a-directory cases with actionable guidance
- Bump CLI version to 0.2.30
Agent: ux-engineer
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>