* test(telemetry): add unit tests for PII scrubbing and PostHog payload structure
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(test): drain stale telemetry events before each test to fix CI flake
The telemetry module is a singleton whose event buffer accumulates
across test files. Other tests (e.g. sprite destroy) can leave events
in the buffer that pollute assertions. Drain + clear mock before each
test action to isolate test state.
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
PostHog's /batch/ endpoint requires distinct_id inside each event's
properties object, not at the event level. Events were silently dropped.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pullChildHistory was awaited after the interactive session, blocking
process.exit() for up to 5+ minutes while it SSHed back into the VM.
This is a convenience feature for `spawn tree` — it should never make
the user wait.
Changed to fire-and-forget: process.exit() fires immediately,
killing any in-flight SSH calls. Headless mode still awaits it
since there's no user waiting.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sends CLI errors, warnings, and crashes to PostHog for observability.
Strictly error/warning events — no command tracking or session events.
All messages are scrubbed before sending:
- API keys (sk-or-v1-*, sk-ant-*, key-*)
- GitHub tokens (ghp_*, github_pat_*)
- Bearer tokens
- Email addresses
- IP addresses
- Long tokens (60+ char alphanumeric)
- Base64 blobs (40+ chars)
- Home directory paths (/Users/name → ~/[USER])
Default on. Disable with SPAWN_TELEMETRY=0.
Fire-and-forget with 5s timeout — never blocks the CLI.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`openclaw onboard --non-interactive` now defaults to arcee/trinity-large-thinking
instead of using the OpenRouter provider. Always run `openclaw config set
agents.defaults.model.primary` after onboard to ensure openrouter/auto is set.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds two behavioral crypto miner checks to the security scan:
- Flag non-agent processes using >80% CPU (catches renamed miners)
- Detect outbound connections to known mining pool ports (3333, 4444, etc.)
Adds a Security column to `spawn status` that shows clean/alerts/—
for each running server, with detailed alert summary after the table.
JSON output includes security and security_alerts fields.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Apply shellQuote() to package names interpolated into startup scripts
across all four cloud providers (GCP, AWS, Hetzner, DigitalOcean).
Defense-in-depth against supply chain attacks where compromised package
lists could inject shell metacharacters into root cloud-init scripts.
Fixes#3216
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Installs a cron job (every 6h) that checks for SSH key anomalies,
failed login attempts (brute-force), suspicious software (attack tools,
crypto miners), unexpected processes, rogue cron entries, and unusual
listening ports. Findings are written to /var/log/spawn-security-alerts.log
and displayed as warnings when users reconnect via `spawn connect`.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass the preview origin via SPAWN_PREVIEW_ORIGIN env var instead of
interpolating it into the Node.js inline script, preventing potential
command injection if a malicious preview URL were returned by the API.
Fixes#3215
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Install GitHub CLI (gh) via the official APT repository in the GCP
cloud-init startup script, so it's available before SSH is reported
as ready. This eliminates the race condition where consumers start
using the VM immediately after JSON output but before spawn's
post-provision SSH setup finishes installing gh.
Fixes#3206
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: complete VM recovery rewrite for spawn fix command
Fixes#3173
Rewrites spawn fix to use CloudRunner interface for full VM recovery
instead of a flat bash script piped over SSH. Now runs the same
install(), configure(), preLaunch() functions as initial provisioning.
- Added generic SSH CloudRunner (ssh-runner.ts) reusable by other commands
- Exported injectEnvVarsToRunner() from orchestrate.ts for shared use
- Fixed command injection vulnerability via validateIdentifier(binaryName)
- Updated dependency injection: runScript → makeRunner (CloudRunner)
- Updated tests to use CloudRunner-based DI pattern
Agent: code-health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ssh-runner): add coverage for validation paths
Tests cover the early-exit branches in makeSshRunner methods
(runServer invalid command, uploadFile/downloadFile path traversal)
that throw before any subprocess is spawned.
Agent: team-lead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Snapshots built on larger server types cause "image disk is bigger than
server type disk" errors on cx23. Remove findSpawnSnapshot and snapshot
logic from Hetzner provisioning so it always uses ubuntu-24.04.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(openclaw): fix telegram bot not responding to messages
The switch to `openclaw config set` calls in #2655 created malformed
nested config structures — the bot token and dmPolicy weren't read
properly by openclaw, so the bot never started polling for messages.
The `groups` block was also dropped entirely.
Fix: write the complete telegram channel object atomically via a bun
script that reads the existing config, deep-merges the full telegram
block, and writes it back — matching the original atomic JSON approach.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): pass telegram config via env var instead of JS interpolation
Prevents JavaScript code injection via attacker-controlled bot token by
passing the telegramConfig JSON through a shell-quoted environment variable
(TELEGRAM_CONFIG) and parsing it with JSON.parse(process.env.TELEGRAM_CONFIG)
inside the bun script, instead of interpolating it directly into JS source.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: add test for atomic telegram config write
Verifies that openclaw telegram config uses a bun merge script (atomic
write) instead of individual `openclaw config set` calls, and that the
full config object (botToken, dmPolicy, groupPolicy, groups) is included.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
The SKILL_BODY and HERMES_SNIPPET in spawn-skill.ts listed available
agents and clouds but were not updated when pi (#3156) and daytona
(#3168) were added. Agents spawned via the skill system could not
delegate work to Pi or provision on Daytona.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
spawn list --clear silently cleared all history in non-interactive mode
(piped stdin, CI, SSH) without any confirmation. This is inconsistent
with spawn delete which requires --yes. Add the same guard so
destructive history clearing requires explicit opt-in when there is no
TTY to show a confirmation prompt.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
cursor was missing from the AGENT_SKILLS map in spawn-skill.ts, causing
spawn skill injection to silently skip cursor VMs when --beta recursive
is active. pi was present in AGENT_SKILLS but missing from all test
arrays in spawn-skill.test.ts.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Poll `openclaw status --json` after onboarding until bootstrapPending
is false (up to 60s). Prevents the Control UI from opening into a
broken state where chat fails with "No session found" because the
initial session hasn't been created yet.
Fixes#3167
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Cursor CLI validates CURSOR_API_KEY before connecting to the configured
endpoint. The dummy value "spawn-proxy" fails validation immediately,
causing an infinite restart loop. Use the actual OPENROUTER_API_KEY as
CURSOR_API_KEY so it passes Cursor's key format check.
Fixes#3166
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The same 12-line saveSpawnRecord block was duplicated 3 times in
runOrchestration() (fast-mode boot, fast-mode retry, sequential path).
A bug fixed in one copy could easily be missed in another. Extracted
a shared recordSpawn() helper that all 3 sites now call.
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The local provider was missing the empty-string and null-byte command
validation that all other cloud providers (AWS, GCP, Hetzner, DO, Sprite)
already enforce. While callers currently pass hardcoded commands, this adds
defense-in-depth parity with the rest of the codebase.
Fixes#3155
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): array-based agent detection and GCP instance name validation
Replace shell string concatenation in detectAgent() with individual
`command -v` calls per agent, eliminating the compound shell command.
Add _gcp_validate_instance_name() to validate GCP instance names match
[a-z][a-z0-9-]*[a-z0-9] before passing to gcloud commands.
Fixes#3151Fixes#3149
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add instance name validation in _gcp_cleanup_stale()
Defense-in-depth: validate instance names from GCP API before passing
to gcloud delete, consistent with validation at other call sites.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
On macOS, lstat("/etc/master.passwd") throws EACCES before the
sensitive-path pattern check runs. Move pattern matching before
filesystem calls so security errors are thrown consistently
regardless of filesystem permissions.
Fixes#3153
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
These security-critical validation functions in local/local.ts had zero
direct test coverage. Adds tests for valid inputs, empty strings,
shell metacharacters, path traversal, and uppercase rejection.
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- spawn feedback: prompt interactively for message when run in a TTY
without arguments, instead of showing an error
- spawn link: report SSH failure after "Connect now?" instead of
silently ignoring the exit code
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace string-interpolated shell commands in pullAndStartContainer()
with Bun.spawn() array arguments, eliminating shell interpretation
as defense-in-depth against command injection.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Reject file paths containing ASCII control characters (ANSI escape
sequences, null bytes, etc.) in validatePromptFilePath() to prevent
terminal injection. Also strip control chars in handlePromptFileError()
as defense-in-depth for error paths before validation.
Fixes#3138
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
These three exported pure functions had zero test coverage. validateScriptTemplate
is security-critical (prevents ${} interpolation injection in script templates).
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#3136 - add path validation to uploadFile/downloadFile in local.ts
Fixes#3135 - add agentName validation before Docker shell commands
- validateLocalPath() resolves paths and rejects ".." traversal attempts
- validateAgentName() ensures agent names match [a-z0-9-]+ before Docker ops
- Both functions are exported for testability
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The --flat flag was documented in help output and used by `spawn list`
but missing from KNOWN_FLAGS, causing an "Unknown flag" error.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Agent config functions (setupClaudeCodeConfig, setupCodexConfig, etc.)
captured the bare host runner from local/agents.ts, bypassing the Docker
wrapper. This caused config files like ~/.claude/settings.json to be
written to the host filesystem instead of inside the sandbox container.
Fix: when --beta sandbox is active, recreate agents with the Docker-wrapped
runner so configure()/install() closures execute inside the container.
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add pre-encoding validation to reject ${} interpolation patterns in
script template strings before they are base64-encoded and injected
into systemd services running with root privileges on remote VMs.
Defense-in-depth against future regressions where template variable
interpolation before encoding could allow command injection.
Fixes#3130
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The sandbox mode now starts the Docker daemon whenever it's not running,
not only after a fresh install. This handles the common case where
OrbStack/Docker is installed but the daemon isn't started yet.
Flow: check daemon → if down, check binary → if missing, install →
start daemon (open -a OrbStack / systemctl start docker) → poll up to 30s
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add --beta sandbox for Docker-based local agent sandboxing
When running agents locally, users can now opt into sandboxed execution
via `--beta sandbox` or the interactive picker. This runs the agent
inside a Docker container (using pre-built ghcr.io/openrouterteam images)
with memory and CPU limits, providing filesystem/network isolation.
- Docker auto-installed if missing (OrbStack on macOS, docker.io on Linux)
- Reuses existing makeDockerRunner() pattern from Hetzner/GCP
- Container auto-cleaned up on process exit
- OpenClaw security warning skipped in sandbox mode (already isolated)
- Interactive picker shows Direct vs Sandboxed when Docker available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rename local machine to local
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
* fix: remove memory limits and move sandbox to cloud picker
- Remove --memory=4g --cpus=2 from docker run (breaks small VMs and recursive spawns)
- Replace sandbox sub-prompt with a "Local Machine (Sandboxed)" option
in the cloud picker itself, shown when --beta sandbox is active
- Docker availability check happens later in local/main.ts (ensureDocker),
not in the picker — so the option always appears with --beta sandbox
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add --beta sandbox to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
custom-flag.test.ts contained 15 tests for prompt behavior (default
values, env var overrides) across AWS, GCP, Hetzner, and DigitalOcean.
Every one of these tests is an exact or near-exact duplicate of tests
already present in the cloud-specific coverage files:
- hetzner-cov.test.ts: promptServerType, promptLocation defaults + env vars
- gcp-cov.test.ts: promptMachineType, promptZone defaults + env vars
- do-cov.test.ts: promptDropletSize, promptDoRegion defaults + env vars
- aws-cov.test.ts: promptRegion, promptBundle env vars
No test coverage was lost — all scenarios remain in the cloud-specific
files with equal or greater assertion depth.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): remove stale paths from biome check step in lint.yml
biome.json restricts linting to packages/**/*.ts via its includes filter,
so passing .claude/scripts/ and .claude/skills/setup-spa/ to the biome
check command was a no-op — biome reported 0 files processed for those
paths and silently skipped them.
Remove the stale paths so the CI step accurately reflects what biome
actually checks.
* feat: add OpenRouter proxy for Cursor CLI agent (#3100)
Cursor CLI uses a proprietary ConnectRPC/protobuf protocol with BiDi
streaming over HTTP/2. It validates API keys against Cursor's own servers
and hardcodes api2.cursor.sh for agent streaming — making direct
OpenRouter integration impossible.
This adds a local translation proxy that intercepts Cursor's protocol
and routes LLM traffic through OpenRouter:
Architecture:
Cursor CLI → Caddy (HTTPS/H2, port 443) → split routing:
/agent.v1.AgentService/* → H2C Node.js (BiDi streaming → OpenRouter)
everything else → HTTP/1.1 Node.js (fake auth, models, config)
Key components:
- cursor-proxy.ts: proxy scripts + deployment functions
- Caddy reverse proxy for TLS + HTTP/2 termination
- /etc/hosts spoofing to intercept api2.cursor.sh
- Hand-rolled protobuf codec for AgentServerMessage format
- SSE stream translation (OpenRouter → ConnectRPC protobuf frames)
Proto schemas reverse-engineered from Cursor CLI binary v2026.03.25:
- AgentServerMessage.InteractionUpdate.TextDeltaUpdate.text
- agent.v1.ModelDetails (model_id, display_model_id, display_name)
- TurnEndedUpdate (input_tokens, output_tokens)
Tested end-to-end on Sprite VM: Cursor CLI printed proxy response with
EXIT=0.
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(digitalocean): use canonical DIGITALOCEAN_ACCESS_TOKEN env var (#3099)
Replaces all references to DO_API_TOKEN with DIGITALOCEAN_ACCESS_TOKEN,
matching DigitalOcean's official CLI and API documentation. This includes
TypeScript source, tests, shell scripts, Packer config, CI workflows,
and documentation.
Supersedes #3068 (rebased onto current main).
Agent: pr-maintainer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove --trust flag from Cursor CLI launch command (#3101)
Cursor CLI v2026.03.25 only allows --trust in headless/print mode.
Launching interactively with --trust causes immediate exit with error.
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
* fix(cursor): set CURSOR_API_KEY to skip browser login (#3104)
Cursor CLI requires authentication before making API calls. Without
CURSOR_API_KEY set, it falls back to browser-based OAuth which fails
because the proxy spoofs api2.cursor.sh to localhost, breaking the
OAuth callback. Setting a dummy CURSOR_API_KEY makes Cursor use the
/auth/exchange_user_api_key endpoint instead, which the proxy already
handles with a fake JWT.
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: sync README with source of truth (#3097)
- update tagline: 8 agents/48 combos -> 9 agents/54 combos
- add Cursor CLI row to matrix table
manifest.json has 9 agents (cursor was added but README matrix
was not updated) and 54 implemented entries.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
* fix(cursor): update proxy model list to current models (#3105)
Replace outdated models (Claude Sonnet 4, GPT-4o) with current ones:
- Claude Sonnet 4.6 (default), Claude Haiku 4.5
- GPT-4.1
- Gemini 2.5 Pro, Gemini 2.5 Flash
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(status): add agent alive probe via SSH (#3109)
`spawn status` now probes running servers by SSHing in and running
`{agent} --version` to verify the agent binary is installed and
executable. Results show in a new "Probe" column (live/down/—) and
as `agent_alive` in JSON output. Only "running" servers are probed;
gone/stopped/unknown servers are skipped.
The probe function is injectable via opts for testability.
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add cursor to agent lists in spawn skill files (#3108)
cursor is a fully implemented agent across all 6 clouds but was missing
from the available agents list in spawn skill instructions injected onto
child VMs. This caused claude, codex, hermes, junie, kilocode, openclaw,
opencode, and zeroclaw to be unaware they could delegate work to cursor.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
* fix(security): expand $HOME before path validation in downloadFile (#3080)
Fixes#3080
Prevents path traversal via other $VAR expansions by normalizing
$HOME to ~ before the strict path regex check, removing the need
to allow $ in the charset.
Applied to all 5 cloud providers:
- digitalocean: downloadFile
- aws: downloadFile
- sprite: downloadFileSprite
- gcp: uploadFile + downloadFile
- hetzner: downloadFile
Also bumps CLI version to 0.27.7.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(manifest): correct cursor repo to cursor/cursor and update star counts (#3092)
The cursor agent's repo was set to anysphere/cursor (private, returns 404),
which caused the stars-update script to store the raw 404 error object as
github_stars instead of a number — breaking the manifest-type-contracts test.
Fix: update repo to the public cursor/cursor repo (32,526 stars as of 2026-03-29).
Also applies the daily star count updates for all other agents.
-- qa/e2e-tester
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
* fix(spawn-fix): load API keys via config file, not just process.env (#3095)
Previously buildFixScript() resolved env templates directly from
process.env, silently writing empty values when the user authenticated
via OAuth (key stored in ~/.config/spawn/openrouter.json). Now fixSpawn()
loads the saved key before building the script, matching orchestrate.ts.
Fixes#3094
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: sync README commands table with help.ts (--prompt, --prompt-file) (#3106)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
* fix(e2e): reduce Hetzner batch parallelism from 3 to 2 (#3112)
Prevents server_limit_reached errors when pre-existing servers (e.g.
spawn-szil) consume quota during E2E batch 1.
Fixes#3111
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor(e2e): normalize unused-arg comments in headless_env functions (#3113)
GCP, Sprite, and DigitalOcean had commented-out code `# local agent="$2"`
in their `_headless_env` functions. Hetzner already used the cleaner style
`# $2 = agent (unused but part of the interface)`. Normalize to match.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests (#3089)
* test: remove duplicate and theatrical tests
- update-check.test.ts: fix 3 tests using stale hardcoded version '0.2.3'
(older than current 0.29.1) to use `pkg.version` so 'should not update
when up to date' actually tests the current-version path correctly
- run-path-credential-display.test.ts: strengthen weak `toBeDefined()`
assertion on digitalocean hint to `toContain('Simple cloud hosting')`,
making it verify the actual fallback hint content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: replace theatrical no-assert tests with real assertions in recursive-spawn
Two tests in recursive-spawn.test.ts captured console.log output into a
logs array but never asserted against it. Both ended with a comment like
"should not throw" — meaning they only proved the function didn't crash,
not that it produced the right output.
- "shows empty message when no history": now spies on p.log.info and
asserts cmdTree() emits "No spawn history found."
- "shows flat message when no parent-child relationships": now asserts
cmdTree() emits "no parent-child relationships" via p.log.info.
expect() call count: 4831 to 4834 (+3 real assertions added).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: consolidate redundant describe block in cmd-fix-cov.test.ts
The file had two separate describe blocks with identical beforeEach/afterEach
boilerplate. The second block ("fixSpawn connection edge cases") contained only
one test ("shows success when fix script succeeds") and could be merged directly
into the first block ("fixSpawn (additional coverage)") without any loss of
coverage or setup fidelity.
Removes 23 lines of duplicated boilerplate. Test count unchanged (6 tests).
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(config): extend biome.json includes to cover .claude/**/*.ts
Add .claude/**/*.ts to biome.json includes so TypeScript files in
.claude/scripts/ and .claude/skills/ are covered by biome formatting.
Linting is disabled for .claude/** via override because the GritQL
plugins (no-try-catch, no-typeof-string-number) target the main CLI
codebase and cannot be scoped per-path — .claude/ hook scripts
legitimately use try/catch as they run standalone outside the package.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(prompts): stop infinite shutdown loop after TeamDelete in non-interactive mode (#3116)
After TeamDelete completes in -p (non-interactive) mode, Claude Code's
harness was re-injecting shutdown prompts every turn. The root cause:
the Monitor Loop instructed the agent to call TaskList + Bash on EVERY
iteration, including after TeamDelete, which kept the session alive so
the harness could inject more shutdown prompts.
Fix: add an explicit EXCEPTION to both refactor-team-prompt.md and
refactor-issue-prompt.md instructing the team lead that after TeamDelete
is called, the very next response MUST be plain text only with no tool
calls. A text-only response is the termination signal for the
non-interactive harness.
Fixes#3103
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(zeroclaw): remove broken zeroclaw agent (repo 404) (#3107)
* fix(zeroclaw): remove broken zeroclaw agent (repo 404)
The zeroclaw-labs/zeroclaw GitHub repository returns 404 — all installs
fail. Remove zeroclaw entirely from the matrix: agent definition,
setup code, shell scripts, e2e tests, packer config, skill files,
and documentation.
Fixes#3102
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(zeroclaw): remove stale zeroclaw reference from discovery.md ARM agents list
Addresses security review on PR #3107 — the last remaining zeroclaw
reference in .claude/rules/discovery.md is now removed.
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(zeroclaw): remove remaining stale zeroclaw references from CI/packer
Remove zeroclaw from:
- .github/workflows/agent-tarballs.yml ARM build matrix
- .github/workflows/docker.yml agent matrix
- packer/digitalocean.pkr.hcl comment
- sh/e2e/e2e.sh comment
Addresses all 5 stale references flagged in security review of PR #3107.
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(cli): allow --headless and --dry-run to be used together (#3117)
Removes the mutual-exclusion validation that blocked combining these flags.
Both flags serve independent purposes: --dry-run previews what would happen,
--headless suppresses interactive prompts and emits structured output.
Combining them is valid for CI pipelines that want structured JSON previews.
Fixes#3114
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(cli): allow --headless and --dry-run to be used together (#3118)
* test: remove redundant theatrical assertions (#3120)
Remove bare toHaveBeenCalled() checks that preceded stronger content
assertions, and strengthen the "shows manual install command" test to
verify the actual install script URL appears in output.
Affected files:
- cmd-update-cov: remove redundant consoleSpy.toHaveBeenCalled() (x2),
strengthen "shows manual install command" to check install.sh content
- update-check: remove redundant consoleErrorSpy.toHaveBeenCalled() (x2)
that were immediately followed by .mock.calls content assertions
- recursive-spawn: remove redundant logInfoSpy.toHaveBeenCalled() before
content check
- cmd-interactive: remove redundant mockIntro/mockOutro.toHaveBeenCalled()
before content checks
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: sync README tagline with manifest (9 agents/54 → 8 agents/48 combinations) (#3119)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* docs: remove stale ZeroClaw references after agent removal (#3122)
ZeroClaw was removed in #3107 (repo 404). Two doc references were left
behind:
- .claude/rules/agent-default-models.md: table row for ZeroClaw model config
- README.md: ZeroClaw listed in --fast skip-cloud-init agent examples
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(e2e): redirect DO max_parallel log_warn to stderr (#3110)
_digitalocean_max_parallel() called log_warn which writes colored output
to stdout, polluting the captured return value when invoked via
cloud_max=$(cloud_max_parallel). The downstream integer comparison
[ "${effective_parallel}" -gt "${cloud_max}" ] then fails with
'integer expression expected', silently leaving the droplet limit cap
unapplied. Fix: redirect log_warn output to stderr so only the numeric
value is captured.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* refactor: remove stale ZeroClaw references from docs and code comments
---------
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Remove bare toHaveBeenCalled() checks that preceded stronger content
assertions, and strengthen the "shows manual install command" test to
verify the actual install script URL appears in output.
Affected files:
- cmd-update-cov: remove redundant consoleSpy.toHaveBeenCalled() (x2),
strengthen "shows manual install command" to check install.sh content
- update-check: remove redundant consoleErrorSpy.toHaveBeenCalled() (x2)
that were immediately followed by .mock.calls content assertions
- recursive-spawn: remove redundant logInfoSpy.toHaveBeenCalled() before
content check
- cmd-interactive: remove redundant mockIntro/mockOutro.toHaveBeenCalled()
before content checks
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the mutual-exclusion validation that blocked combining these flags.
Both flags serve independent purposes: --dry-run previews what would happen,
--headless suppresses interactive prompts and emits structured output.
Combining them is valid for CI pipelines that want structured JSON previews.
Fixes#3114
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: remove duplicate and theatrical tests
- update-check.test.ts: fix 3 tests using stale hardcoded version '0.2.3'
(older than current 0.29.1) to use `pkg.version` so 'should not update
when up to date' actually tests the current-version path correctly
- run-path-credential-display.test.ts: strengthen weak `toBeDefined()`
assertion on digitalocean hint to `toContain('Simple cloud hosting')`,
making it verify the actual fallback hint content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: replace theatrical no-assert tests with real assertions in recursive-spawn
Two tests in recursive-spawn.test.ts captured console.log output into a
logs array but never asserted against it. Both ended with a comment like
"should not throw" — meaning they only proved the function didn't crash,
not that it produced the right output.
- "shows empty message when no history": now spies on p.log.info and
asserts cmdTree() emits "No spawn history found."
- "shows flat message when no parent-child relationships": now asserts
cmdTree() emits "no parent-child relationships" via p.log.info.
expect() call count: 4831 to 4834 (+3 real assertions added).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: consolidate redundant describe block in cmd-fix-cov.test.ts
The file had two separate describe blocks with identical beforeEach/afterEach
boilerplate. The second block ("fixSpawn connection edge cases") contained only
one test ("shows success when fix script succeeds") and could be merged directly
into the first block ("fixSpawn (additional coverage)") without any loss of
coverage or setup fidelity.
Removes 23 lines of duplicated boilerplate. Test count unchanged (6 tests).
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously buildFixScript() resolved env templates directly from
process.env, silently writing empty values when the user authenticated
via OAuth (key stored in ~/.config/spawn/openrouter.json). Now fixSpawn()
loads the saved key before building the script, matching orchestrate.ts.
Fixes#3094
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#3080
Prevents path traversal via other $VAR expansions by normalizing
$HOME to ~ before the strict path regex check, removing the need
to allow $ in the charset.
Applied to all 5 cloud providers:
- digitalocean: downloadFile
- aws: downloadFile
- sprite: downloadFileSprite
- gcp: uploadFile + downloadFile
- hetzner: downloadFile
Also bumps CLI version to 0.27.7.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
cursor is a fully implemented agent across all 6 clouds but was missing
from the available agents list in spawn skill instructions injected onto
child VMs. This caused claude, codex, hermes, junie, kilocode, openclaw,
opencode, and zeroclaw to be unaware they could delegate work to cursor.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
`spawn status` now probes running servers by SSHing in and running
`{agent} --version` to verify the agent binary is installed and
executable. Results show in a new "Probe" column (live/down/—) and
as `agent_alive` in JSON output. Only "running" servers are probed;
gone/stopped/unknown servers are skipped.
The probe function is injectable via opts for testability.
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>