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>
Drops the `issues: opened` trigger and the issue-closing branch from
the gate workflow. PRs from non-collaborators are still auto-closed
(scripted contributions are higher-risk than feedback). Issues stay
open — agents already gate replies on collaborator status, so external
issues simply sit untouched instead of being auto-closed with a stock
message.
Raw `gh issue list` / `gh pr list` in agent prompts bypassed the
bash collaborator gate, letting Claude read non-collaborator issues
(potential prompt injection vector). All prompts now pipe through
a jq filter using the cached collaborator list.
- Added collaborator gate section to _shared-rules.md
- Patched 10 prompt files with inline jq collaborator filter
- High-risk: community-coordinator, security-issue-checker,
qa-record-keeper, security-scanner (read issue bodies)
- Lower-risk: PR list commands in refactor/security prompts
Co-authored-by: Claude Opus 4.6 (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>
When the repo goes public, anyone can open issues/PRs. The agent team
must only engage with collaborators — external submissions are invisible.
Shell scripts (refactor, security, qa): source collaborator-gate.sh and
exit 0 if SPAWN_ISSUE author is not a collaborator. The bots never see
the issue — no comment, no triage, no response.
Prompts (discovery issue-responder, refactor community-coordinator,
security issue-checker): check gh api collaborators endpoint before
engaging with any issue.
Collaborator list is cached for 10 minutes to avoid API rate limits.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.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>
The xeng_approve and xeng_edit_submit handlers marked the reply as
approved in state.db but never called postToX(). Replies were silently
stuck in "ready to post on X" limbo forever.
Both handlers now call postToX(replyText, sourceTweetId) so the reply
goes out as an actual threaded reply on X, and the Slack card shows
the live tweet URL. Mirrors the tweet_approve flow.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.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>
Per product decision, X/Twitter replies should not include the
'(disclosure: i help build this)' attribution. Reddit disclosures
in growth-prompt.md are unchanged.
Co-authored-by: Claude <claude@anthropic.com>
- growth.sh: guard Phase 0b on X_CLIENT_ID (was checking stale X_API_KEY)
- x-fetch.ts: rewrite to use OAuth 2.0 Bearer tokens from state.db w/ auto-refresh
- Strip em/en dashes from all generated JSON output (tweet, engagement, reddit)
- Tighten prompt language against em dashes in all 3 growth prompts
- SPA system prompt: tell Claude how to post tweets via x-post.ts and query
tweets/candidates tables from state.db for context-aware Twitter conversations
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add x-post.ts script for posting tweets via X API v2 (OAuth 1.0a)
- Wire postToX() into SPA's tweet_approve and tweet_edit_submit handlers
- Approved tweets now post directly to X instead of just marking "ready"
- Slack card updates with link to live tweet on success, error msg on failure
- Add X_API_KEY/SECRET/ACCESS_TOKEN/SECRET env vars to SPA environment
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#3325
Agent: code-health
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>
* test(skills): add unit tests for getAvailableSkills filtering
getAvailableSkills() had zero test coverage despite being the entry
point for --beta skills flag filtering. Covers: empty manifest, agent
mismatch, correct filtering, isDefault flag, envVars collection.
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test(skills): add coverage for promptSkillSelection, collectSkillEnvVars, installSkills
The Mock Tests CI check was failing because importing skills.ts in
tests caused bun to instrument it for coverage, but only getAvailableSkills
was tested (12.5% function coverage). Added tests for the remaining
exported functions to bring coverage above the 50% threshold.
Agent: pr-maintainer
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.5 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Two user-facing reconnect hints missed by #3311 still showed
'spawn connect <name>', which is not a registered command. Users
following the hint get 'Unknown agent or cloud: connect'.
Agent: code-health
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.
Tracks whether installs came from Reddit, X, or organic by baking a
ref tag into the install command.
Growth bot shares:
curl -fsSL ... | SPAWN_REF=reddit bash
curl -fsSL ... | SPAWN_REF=x bash
install.sh: if SPAWN_REF is set, sanitizes it (alphanumeric + hyphens,
max 32 chars) and writes to ~/.config/spawn/.ref. Only written once —
never overwritten on updates.
index.ts: on startup, reads .ref and sets it as telemetry context via
setTelemetryContext("ref", ref). Every PostHog event (funnel, lifecycle,
errors) now carries ref=reddit or ref=x for attributed installs, or no
ref for organic.
PostHog query: filter any event by ref=reddit to see "how many Reddit-
sourced users made it through the funnel" vs organic.
Bumps 1.0.15 -> 1.0.16.
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
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.
Phase 2+3 of the token-savings plan (follows #3310 which reduced cron
frequency and downgraded team leads to Sonnet).
Extracts duplicated rules into _shared-rules.md (72 lines) and moves
teammate-specific protocols into individual micro-prompts that team
leads read on-demand via Read tool instead of carrying in every turn.
New: _shared-rules.md + teammates/ directory (16 files, 246 lines)
Rewritten: 4 team prompts from 1,199 total lines to 243 (80% reduction)
refactor-team-prompt.md 319 -> 67 (79%)
security-review-all-prompt.md 245 -> 64 (74%)
qa-quality-prompt.md 302 -> 43 (86%)
discovery-team-prompt.md 333 -> 69 (79%)
Also merges shell-scanner + code-scanner into one scanner teammate
for security reviews (4 -> 3 teammates per cycle).
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
* feat(growth): add Phase 0 — daily tweet draft + X mention engagement
Adds a new Phase 0 to the growth agent cycle that runs before Reddit
scanning:
Phase 0a — Tweet Draft (always runs):
- Gathers last 7 days of git commits
- Claude drafts a single ≤280 char tweet about features, fixes, or best
practices
- Posts Block Kit card to #C0ARSCAP4MN with Approve/Edit/Skip buttons
Phase 0b — X Mention Search (runs only if X_API_KEY is set):
- x-fetch.ts searches X API v2 for Spawn/OpenRouter mentions
- Claude scores mentions and drafts engagement replies
- Posts engagement card to #C0ARSCAP4MN with approval buttons
- Gracefully skips when no X credentials are configured
All cards require human approval — nothing is ever auto-posted.
New files:
- tweet-prompt.md: Claude prompt for tweet generation
- x-engage-prompt.md: Claude prompt for X engagement scoring
- x-fetch.ts: X API v2 search client with OAuth 1.0a
Modified files:
- growth.sh: Phase 0a + 0b insertion, cleanup trap updates
- helpers.ts: tweets table schema, TweetRow CRUD, logTweetDecision()
- main.ts: TweetPayloadSchema, XEngagePayloadSchema, postTweetCard(),
postXEngageCard(), 8 new Slack action handlers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update URL format in tweet prompt guidelines
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
* Update URL for Spawn reference in engagement prompt
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
---------
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Claude scoring phase has been timing out at the 600s mark when
processing 500+ Reddit posts. Bump to 1800s (30 min) to give
enough headroom for large post sets.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
"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>
Two high-impact, zero-risk changes to get daily agent team spend under $50:
1. Reduce cron frequency:
- Security: */30 → every 4 hours (48→6 cycles/day, 87% reduction)
- Refactor: */15 → every 2 hours (96→12 cycles/day, 87% reduction)
Most cycles find nothing to do (no new PRs/issues). Issue-triggered runs
(on labeled issues) still fire instantly via the `issues` event type,
so response time to real work is unchanged. The trigger-server already
returns 409 when a cycle is in-progress, so high cron frequency was just
idle-polling cost.
2. Downgrade team-lead model from Opus to Sonnet:
- Security: --model sonnet for review_all and scan modes (triage was
already using gemini-3-flash-preview)
- Refactor: --model sonnet
The team lead's job is coordination — spawn teammates, monitor them,
shut down. This is routing, not reasoning. Sonnet handles it fine and
its output tokens are ~5x cheaper than Opus. Teammates (spawned by the
lead) use their own model flags and are unaffected.
Combined effect: ~90% fewer cycles × ~80% cheaper per cycle on the team
lead = estimated 95%+ cost reduction on team-lead tokens alone.
Follow-up PR will trim prompt sizes (Phase 2) and consolidate security
teammates (Phase 3) per the plan, but this Phase 1 closes most of the gap.
Co-authored-by: L <6723574+louisgv@users.noreply.github.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>
* feat(telemetry): funnel + lifecycle events for onboarding drop-off
Adds low-volume, high-signal product events on top of the existing
errors/warnings telemetry (shared/telemetry.ts). Answers "where do users
bail before reaching a running agent" at the fleet level.
Funnel events (in orchestrate.ts, both fast and sequential paths):
funnel_started pipeline begins
funnel_cloud_authed cloud.authenticate() ok
funnel_credentials_ready OR key + preProvision resolved
funnel_vm_ready VM booted and SSH-reachable
funnel_install_completed agent install succeeded (tarball or live)
funnel_configure_completed agent.configure() ran
funnel_prelaunch_completed gateway / dashboard / preLaunch hooks done
funnel_handoff about to launch TUI (final step)
Every event carries elapsed_ms since funnel_started, plus agent and cloud
via telemetry context. Per-step counts reveal the drop-off funnel in
PostHog without touching any PII.
Lifecycle events (new shared/lifecycle-telemetry.ts):
spawn_connected { spawn_id, agent, cloud, connect_count, date }
fired from list.ts when the user reconnects via the interactive picker.
Increments connection.metadata.connect_count and writes last_connected_at
so subsequent events and the eventual spawn_deleted have the total.
spawn_deleted { spawn_id, agent, cloud, lifetime_hours, connect_count, date }
fired from delete.ts (both interactive confirmAndDelete and headless
cmdDelete loop) after a successful cloud destroy. lifetime_hours is
computed from SpawnRecord.timestamp to now. Clamped at 0 for corrupt
clocks. connect_count is read from metadata.
New captureEvent(name, properties) helper in telemetry.ts:
- Respects SPAWN_TELEMETRY=0 opt-out (no new flag)
- Runs every string property through the existing scrubber (API keys,
GitHub tokens, bearer, emails, IPs, base64 blobs, home paths)
- Non-string values pass through untouched
Tests: 20 new (15 lifecycle-telemetry + 2 captureEvent + 3 assertion
additions to disabled-telemetry). Full suite: 2129/2129 pass.
Bumps 1.0.10 -> 1.0.11. Patch bump — auto-propagates under #3296 policy.
* fix(test): replace mock.module with spyOn in lifecycle-telemetry tests
mock.module contaminates the global module registry when running under
--coverage, causing telemetry.test.ts and history-cov.test.ts to receive
mocked implementations instead of the real modules. Switch to spyOn with
mockRestore in afterEach so the real modules are preserved across files.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: L <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>
* fix(security): use temp file for GitHub token to avoid process listing exposure
Fixes#3300
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(security): pass GitHub token via heredoc instead of local temp file
The previous fix wrote the token to a temp file on the LOCAL host, but
the command string was executed on the REMOTE server via runner.runServer(),
so `cat` would fail with 'No such file or directory'. Switch to a heredoc
which is parsed by the remote shell and never appears in /proc/*/cmdline.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(security): upload token to remote via SCP instead of heredoc
The previous heredoc approach (`cat <<'EOF'`) doesn't work because all
cloud runners wrap commands in `bash -c ${shellQuote(cmd)}`, and heredocs
are not valid inside single-quoted bash -c strings.
Use runner.uploadFile() (SCP) to place the token on the remote server as
a temp file (mode 0600), then cat+rm it in the remote command. This is
the same proven pattern used by uploadConfigFile(). The local temp file
is always cleaned up after upload, and the remote temp file is cleaned up
both on success (inline rm) and on failure (best-effort rm).
Agent: security-auditor
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.5 <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>
Closes a batch of real security findings filed against growth.sh and reddit-fetch.ts.
growth.sh:
- Switch all four `bun -e "...${VAR}..."` sites to env-var passing
(_VAR="..." bun -e 'process.env._VAR'), per .claude/rules/shell-scripts.md.
Closes#3188, #3221, #3223.
- Spawn claude under `setsid` so it owns its own process group, and kill the
group via `kill -SIG -PGID` instead of racing with pkill -P. Adds a numeric
guard on CLAUDE_PID. Closes#3193, #3205.
- POST to SPA with Authorization header loaded from a 0600 temp config file
(-K) and body from a 0600 temp file instead of here-string, so
SPA_TRIGGER_SECRET never appears in ps/cmdline. Closes#3224.
- Drop dead REDDIT_JSON=$(cat ...) line.
- Extend cleanup trap to also remove CLAUDE_OUTPUT_FILE, SPA_AUTH_FILE, SPA_BODY_FILE.
reddit-fetch.ts:
- Validate REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET don't contain ':' or CRLF
(prevents Basic-auth corruption and header injection). Closes#3198.
- Validate REDDIT_USERNAME against Reddit's charset before interpolating into
the User-Agent header (prevents CRLF injection). Closes#3207.
- Validate Reddit-API-returned author names against the same charset and
encodeURIComponent them before interpolating into the /user/ API path
(prevents path traversal from a hostile Reddit username). Closes#3202.
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>
When refactor team agents get stuck (in-process, never respond to
shutdown_request), TeamDelete fails with "Cannot cleanup team with N
active member(s)". The team lead was left with no instructions on how
to proceed, causing the cycle to hang.
Fix: update step 4 of the shutdown sequence to:
1. Call TeamDelete (proceed regardless of success or failure)
2. Manually remove team files as fallback:
rm -f ~/.claude/teams/spawn-refactor.json
rm -rf ~/.claude/tasks/spawn-refactor/
3. Run git worktree prune + rm -rf worktree in same turn
4. Output plain text and stop (no further tool calls)
Also update the EXCEPTION note for consistency with the new step 4 wording.
Fixes#3281
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds the Daytona icon (from their GitHub org avatar) so the cloud
picker shows a proper logo instead of a text "D" placeholder.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Remove the 1h cache-first path that caused 14-day stale manifests.
Every run now fetches fresh from GitHub (3s timeout). Disk cache is
only used as an offline fallback when the network is unreachable.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.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>
* fix(security): validate env var keys in skill injection (orchestrate.ts)
Fixes#3269
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add base64 validation for defense-in-depth in skill env injection
Add validation of base64-encoded values to match the existing pattern
in injectEnvVarsToRunner (line 518), providing defense-in-depth even
though base64 output is highly unlikely to contain invalid characters.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): base64-encode entire skill env payload before shell interpolation
Matches the injectEnvVarsToRunner pattern: base64-encode the full payload
and decode on the remote side, eliminating any shell interpolation of
individual env lines. Addresses review feedback on double-evaluation risk.
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.6 <noreply@anthropic.com>
Claude scoring has been timing out since Apr 10 — the 5-min limit
is too tight for 500+ post sets. Bumping to 10 min to match observed
scoring times.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <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>
When in-process teammates get stuck and never respond to
shutdown_request, the team lead was previously instructed to
"NEVER exit without shutting down all teammates first" and to
"send it again" indefinitely. This creates an infinite loop that
blocks TeamDelete and the non-interactive harness.
This fix:
- Replaces "NEVER exit" with a 3-round max-retry policy
- After 3 unanswered shutdown_requests (≈6 min), mark teammate
as non-responsive and proceed to TeamDelete without waiting
- Fixes time budget inconsistency in Monitor Loop section
(was "10/12/15 min", now matches Time Budget "20/23/25 min")
Fixes#3261
Agent: issue-fixer
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>