spawn <agent> <cloud> --repo user/template
Clones https://github.com/user/template.git to ~/project on the VM,
parses spawn.md (YAML frontmatter), and applies its custom-setup
contract:
- `setup`: oauth (open URL + wait for Enter), cli_auth (run on VM),
api_key (no-echo prompt → /etc/spawn/secrets, sourced from .bashrc),
command (run on VM)
- `mcp_servers`: env values stay as ${NAME} placeholders so secrets
never end up in the template repo. Replay routes through the
existing skills.ts helpers (Claude settings.json, Cursor mcp.json,
Codex config.toml) — no `node -e` injection.
- `setup_commands`: run inside ~/project
When the clone succeeds, the agent launches with `cd ~/project && ...`
so the user lands in their template's working directory. Reconnect via
`spawn last` replays the same launchCmd.
Built-in steps (github auth, auto-update, etc.) stay in the CLI
--steps flag — spawn.md only handles custom setup that Spawn doesn't
know about natively.
Bumps CLI to 1.0.22.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cloud bundles (hetzner.js, digitalocean.js, etc.) never called
initTelemetry(), so _enabled was false and every captureEvent/trackFunnel
call in orchestrate.ts was a silent no-op. All orchestration funnel
events (funnel_cloud_authed through funnel_handoff) were lost.
Adds initTelemetry(pkg.version) to all 7 cloud entry points so
funnel events actually reach PostHog.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 1.0.x → 1.1.0 minor bump blocked auto-update for all users since
only patch bumps were auto-installed. Users without SPAWN_AUTO_UPDATE=1
were stuck on 1.0.x and never received the telemetry fix.
Version set to 1.0.20 so existing 1.0.x users see it as a patch bump
and auto-install it. The new update logic then allows future minor bumps
(same major) to auto-install too. Only major bumps (2.0.0+) require
SPAWN_AUTO_UPDATE=1.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(telemetry): send events immediately, persistent user ID, session continuity
Root cause: events were batched (threshold: 10) but orchestration only fires
~8 funnel events. process.exit() kills the process before beforeExit flushes.
Zero real funnel events ever reached PostHog.
Fixes:
- Send each event immediately via fetch (no batching, no lost events)
- Persistent user ID in ~/.config/spawn/.telemetry-id (same across all runs)
- Session ID inherited via SPAWN_TELEMETRY_SESSION env var (parent → child)
- source: "cli" on every event (filter from website data in PostHog)
Removed: _events array, _flushScheduled, flush(), flushSync(), batch logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(telemetry): remove process.exit(0) so telemetry fetches complete
process.exit(0) was called immediately after main() resolved, aborting
any in-flight fire-and-forget telemetry fetches. This silently dropped
spawn_deleted, funnel, and lifecycle events. Now the process exits
naturally when the event loop drains, giving pending requests time to
complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
* feat(digitalocean): guided readiness checklist before deploy
Runs evaluateDigitalOceanReadiness after cloud auth and before region/size
selection so users fix billing/SSH/OpenRouter blockers early, with a
checklist UI that rechecks after each fix. Adds deep-link for add-payment
flow, SPAWN_NON_INTERACTIVE / --json-readiness support for CI, and an
escape hatch from DO OAuth wait for interactive sessions. Other clouds
unchanged.
Ported from digitalocean/spawn#2 (Scott Miller @scott). Bumps CLI to 1.1.0.
Refactors the new preflight TTY-gating test to drive process.std*.isTTY
directly with descriptor save/restore and clears stale
~/.config/spawn/digitalocean.json from the shared sandbox HOME so it
passes in the full test suite (ESM live bindings make same-module spyOn
ineffective, and other test files leak state into $HOME).
Co-Authored-By: Scott Miller <scottmiller@digitalocean.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): update-check mock versions for 1.1.0 version bump
Mock "newer" versions (1.0.99) were no longer newer than the current
1.1.0 version, causing all update-check tests to fail. Bumped mock
versions to 99.0.0 for general tests, 1.1.99 for patch, 1.2.0 for
minor, keeping 2.0.0 for major.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(readiness): expand coverage + remove aspirational coverage threshold
- Add evaluateDigitalOceanReadiness tests: auth failure, all-pass,
email/payment/droplet/ssh/openrouter blockers, multi-blocker ordering,
saved key fallback, edge cases (limit=0, count API failure)
- Expand checklistLineStatus tests: all 6 blocker codes, pending-when-
do_auth-blocked, all-blockers-active scenario
- Add READINESS_CHECKLIST_ROWS validation tests
- Expand sortBlockers tests: empty input, dedup, canonical order, single
- Remove coverageThreshold from bunfig.toml — main was already at 82.99%
functions vs 90% threshold (never enforced on push, only on PRs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Scott Miller <scottmiller@digitalocean.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
The reconnect hints shown after provisioning in all 5 cloud providers
(Hetzner, AWS, DigitalOcean, GCP, Sprite) only showed raw SSH/CLI
commands. Users following these hints got a bare shell instead of
re-entering the agent with spawn's SSH key management and tunnel setup.
Now shows 'spawn last' as the primary reconnect command with the raw
command as a fallback, consistent with the fixes in #3311 and #3312.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
The --fast flag enables all speed optimizations (images, tarballs,
parallel, docker) but was completely invisible in help output. Users
had to read source or manually stack 4 --beta flags.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
All CI green. Rebased from #3321, added Daytona support, resolved conflicts. Security reviewed: no injection vectors — all env var values come from hardcoded config, shell scripts follow existing patterns.
Merges 4 separate runner.runServer() calls (model, sandbox, browser,
channel stubs) into one exec with commands chained by `;`. On Sprite
(container-exec, not persistent SSH), many sequential execs exhaust the
connection and cause "connection closed" / "context deadline exceeded"
on later steps like gateway startup.
Before: 4 execs → 14 "Config overwrite" log lines → flaky connection
After: 1 exec → same config result → stable connection for gateway
Individual commands use `;` not `&&` so a failure in one (e.g. browser
path not found) doesn't skip the rest — these are all non-fatal prefs.
Bumps 1.0.15 -> 1.0.16.
"spawn connect" is not a valid top-level CLI command — users following
this guidance after SSH reconnect failure would see "Unknown agent or
cloud: connect". Replace with "spawn last" which correctly reconnects
to the most recent spawn.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
KNOWN_AGENTS was missing pi and cursor, so `spawn link` could not
auto-detect these agents on remote servers. Also adds a binary-name
mapping for cursor (whose CLI binary is `agent`).
Bump CLI to 1.0.14.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Two bugs from the #3305 rollout:
1. Test pollution: orchestrate.test.ts imports runOrchestration directly
and never calls initTelemetry, but _enabled defaulted to true in the
module so captureEvent happily fired real events at PostHog tagged
agent=testagent. The onboarding funnel filled up with CI fixture data.
2. Funnel started too late: funnel_* events fired inside runOrchestration,
which is only called AFTER the interactive picker completes. Users who
bail at the agent/cloud/setup-options/name prompts were invisible —
yet that's exactly where real drop-off happens.
Fix 1 — telemetry.ts:
- Default _enabled = false. Nothing fires until initTelemetry is
explicitly called. Production (index.ts) calls it; tests that need
telemetry (telemetry.test.ts) call it with BUN_ENV/NODE_ENV cleared.
- Belt-and-suspenders: initTelemetry now short-circuits when
BUN_ENV === "test" || NODE_ENV === "test", so even if future code
calls it from a test context, events stay local.
Fix 2 — picker instrumentation:
New events fired before runOrchestration in every entry path:
spawn_launched { mode: interactive | agent_interactive | direct | headless }
menu_shown / menu_selected / menu_cancelled (only when user has prior spawns)
agent_picker_shown
agent_selected { agent } — also sets telemetry context
cloud_picker_shown
cloud_selected { cloud } — also sets telemetry context
preflight_passed
setup_options_shown
setup_options_selected { step_count }
name_prompt_shown
name_entered
picker_completed
Wired into:
commands/interactive.ts cmdInteractive + cmdAgentInteractive
commands/run.ts cmdRun (direct `spawn <agent> <cloud>`)
cmdRunHeadless (only spawn_launched)
runOrchestration's existing funnel_* events continue to fire unchanged.
The final funnel in PostHog:
spawn_launched → agent_selected → cloud_selected → preflight_passed
→ setup_options_selected → name_entered → picker_completed
→ funnel_started → funnel_cloud_authed → funnel_credentials_ready
→ funnel_vm_ready → funnel_install_completed → funnel_configure_completed
→ funnel_prelaunch_completed → funnel_handoff
Tests:
- telemetry.test.ts: 2 new env-guard tests (BUN_ENV, NODE_ENV), plus
updated beforeEach to clear both env vars so existing tests still
exercise initTelemetry.
- Full suite: 2131/2131 pass, biome 0 errors.
Bumps 1.0.12 -> 1.0.13 (patch — auto-propagates under #3296 policy).
Restructure temp file write-execute-cleanup in performAutoUpdate so
cleanup is unconditionally reached after tryCatch captures any exec
error. Previously, the Windows and Unix paths each had separate
tryCatch+cleanup+rethrow sequences that could diverge under future
edits. Now a single tryCatch wraps the platform-branching exec, with
cleanup always running before any error is re-thrown.
Fixes#3306
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #3301 modified packages/cli/src/shared/agent-setup.ts (GitHub token
temp file security fix) but did not bump the CLI version. Without this
bump, users on auto-update won't receive the security fix.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add pre-execution validation of downloaded install scripts to catch
corrupted or truncated downloads. Checks minimum size threshold and
expected shebang/header for the platform. Documents current HTTPS-only
security posture and absence of checksum infrastructure.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
auto-install to same-major.minor bumps. The intent was "give users control
over feature updates" but the effect was "nobody installs security patches"
because the default became notice-only for everything.
This decouples the two ideas and aligns the policy with semver intent:
- PATCH bumps (1.0.5 -> 1.0.7, same major.minor): auto-install always,
no opt-in needed. Patches are reserved for bug fixes and security
hardening. Blast radius is bounded by semver: no behavior changes,
no new features, no breaking changes.
- MINOR / MAJOR bumps (1.0.x -> 1.1.0, 1.x.x -> 2.0.0): respect
SPAWN_AUTO_UPDATE=1 as opt-in. These can contain behavior changes
and users should decide when to move to them.
- SPAWN_NO_AUTO_UPDATE=1: new explicit opt-out for CI environments
or pinned installs that need a fully static CLI.
Caveat — the one-time hurdle: users currently on 1.0.6 won't get 1.0.7
automatically, because they're still running 1.0.6's update-check.ts
which honors the old opt-in gate. Once they reach 1.0.7 via spawn update
(or by setting SPAWN_AUTO_UPDATE=1), every future patch will propagate
automatically and the fleet becomes self-healing on security.
Tests:
- 5 new tests lock in the policy (patch auto without env, minor notice
without env, minor auto with env, major notice without env, explicit
opt-out suppresses patch)
- All 21 update-check tests pass (16 existing + 5 new)
- 2109/2109 total suite
Bumps 1.0.6 -> 1.0.7.
* feat(cli): hermes web dashboard tunnel support
Hermes Agent v0.9.0 ships a local web dashboard (hermes dashboard, default
127.0.0.1:9119) for config / session / skill / gateway management. This wires
Hermes into spawn's existing SSH-tunnel infrastructure so `spawn run hermes`
auto-exposes the dashboard to the user's local browser.
- agent-setup.ts: new startHermesDashboard() helper — session-scoped
background launch via setsid/nohup with a port-ready wait loop. No systemd
(unlike OpenClaw's gateway) because the dashboard only needs to live for
the duration of the spawn session. Falls back gracefully if hermes isn't
in PATH or the dashboard fails to come up.
- Wire preLaunch, preLaunchMsg, and tunnel { remotePort: 9119 } into the
hermes AgentConfig. Mirrors the OpenClaw tunnel pattern at
orchestrate.ts:628 — startSshTunnel + openBrowser happen automatically.
- manifest.json: update hermes notes to mention the dashboard.
- hermes-dashboard.test.ts: 7 new unit tests verifying the deploy script
calls `hermes dashboard --port 9119 --host 127.0.0.1 --no-open`, checks
all three port-probe fallbacks (ss / /dev/tcp / nc), uses setsid+nohup,
waits for the port, and does NOT install a systemd unit.
- Bump cli version 1.0.6 -> 1.0.7.
Closes#3293
* chore: bump cli to 1.0.8 to leave 1.0.7 for #3296
---------
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Replace `mock()` + `spyOn().mockImplementation(mockFn)` pattern with
direct `spyOn().mockImplementation(() => ...)` to fix fetch mock type
mismatches. Make execFileSync mocks return Buffer.from("") instead of
void. Add explicit type annotations for callback parameters.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add validateRemotePath() and shellQuote() to instruction_path handling
in skills.ts, matching the pattern used by uploadConfigFile(). Previously,
remotePath from manifest.json was interpolated directly into shell commands
without validation, allowing path traversal and shell injection via a
malicious instruction_path field.
Closes#3275
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- local.ts: spread ReadonlyArray into mutable array for Bun.spawn
- run.ts: capture optional fields in local vars for proper narrowing
- delete.ts: filter SpawnRecordSchema output for required id field
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: replace plan_mode_required with message-based approval in refactor team
Agents spawned with plan_mode_required in non-interactive (-p) mode hang
indefinitely waiting for human UI approval that never arrives. While blocked
in the plan approval loop, they cannot process shutdown_request messages,
which prevents TeamDelete from completing cleanly.
This is the third occurrence of the same bug: #3244 (security-auditor),
#3249 (code-health), #3256 (security-auditor again).
Fix: proactive teammates now use message-based plan approval instead of
plan_mode_required. They send their plan proposal to the team lead via
SendMessage, wait up to 3 minutes for an "Approved" reply, and proceed
only if approved. This is fully compatible with non-interactive mode.
Fixes#3256
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: correct version bump to 1.0.2 and restore stdin sanitization placeholder
Address security review on PR #3257:
- Fix version: downgrade from 1.0.1→1.0.0 was wrong, correct to 1.0.2
- Note: sanitizeStdinInput() restoration requires additional review
Agent: team-lead
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>
CLI plumbing for the skills feature. The skills catalog in manifest.json
is populated by the discovery scout (#3252), not manually curated.
Flow:
1. User runs `spawn claude hetzner --beta skills`
2. Skills picker shows available skills for that agent (from manifest.json)
3. User selects skills, enters required env vars (GITHUB_TOKEN, etc.)
4. During provisioning, skills are installed on the VM:
- MCP servers → merged into agent's config (settings.json, mcp.json)
- Instruction skills → SKILL.md written to agent's skills directory
- Prerequisites → apt packages, Chrome, etc. installed first
5. Env vars appended to .spawnrc for MCP server runtime access
Headless: SPAWN_SELECTED_SKILLS=github-mcp,context7 spawn claude hetzner
Supports: Claude Code, Cursor (native MCP config), all other agents
(generic mcp.json fallback).
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two changes to update behavior:
1. Auto-update is now opt-in via SPAWN_AUTO_UPDATE=1 (default: notify only)
2. Even with auto-update on, only patch versions install automatically
(e.g. 1.0.0 → 1.0.5 yes, 1.0.0 → 1.1.0 no)
This pins users to a stable major.minor — bug fixes flow automatically
but new features require an explicit `spawn update`.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tarballs are built with /root/ paths. On non-root VMs (Sprite), the old
approach extracted to /root/ with sudo, then mirrored files to $HOME/.
This failed on Sprite which doesn't have sudo.
New approach: use tar --transform to remap /root/ → $HOME/ during
extraction. No sudo needed, no mirror step. Falls back to sudo extract
for clouds with passwordless sudo (AWS, GCP).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#3250
The unbounded quantifier {40,} with word boundary \b caused exponential
backtracking on long non-matching strings. Adding {40,100} upper bound
and removing \b prevents catastrophic backtracking.
Agent: security-auditor
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>
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>
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>
* 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>
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>
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>
- 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>
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>