Skip to content

feat(cli): spawn export — capture a claude session into a github repo#3377

Merged
la14-1 merged 6 commits intoOpenRouterTeam:mainfrom
AhmedTMM:spawn-export
May 2, 2026
Merged

feat(cli): spawn export — capture a claude session into a github repo#3377
la14-1 merged 6 commits intoOpenRouterTeam:mainfrom
AhmedTMM:spawn-export

Conversation

@AhmedTMM
Copy link
Copy Markdown
Collaborator

@AhmedTMM AhmedTMM commented May 1, 2026

Summary

Adds `spawn export [name|id]` as a top-level subcommand. Captures a running claude spawn into a redistributable GitHub repo whose README contains the canonical re-spawn command — the symmetric inverse of `--repo`. Today `spawn claude hetzner --repo user/template` consumes a repo; after this lands, `spawn export` produces one.

```
$ spawn export
✓ Exported to https://github.com/alice/my-vm

Re-spawn with:
spawn claude hetzner --repo alice/my-vm
```

What gets exported

  • `project/` — working tree at `~/project` (aggressive default `.gitignore`)
  • `claude/` — sanitized `~/.claude/` (skills, commands, hooks, CLAUDE.md, AGENTS.md, settings.json with token-shaped fields stripped)
  • `spawn.md` — re-spawn metadata
  • `README.md` — quickstart + first-run checklist (renders nicely on GitHub)

Scope decisions

  • Claude only for v1. Other agents return a clear "not yet supported" error.
  • Mechanical capture, not agent-introspected. Followup: tell claude on the VM to read its own session history and enumerate MCPs / OAuth providers into `spawn.md` setup steps.
  • No `--beta` gate — ships as a real feature.
  • All SSH-backed clouds supported. Local cloud currently routes through the same `SshRunner` path; a dedicated local branch is a small followup.
  • Private by default with an interactive visibility prompt.

Files

File Change
`packages/cli/src/commands/export.ts` New (~400 lines) — main command
`packages/cli/src/tests/export.test.ts` New — 18 tests
`packages/cli/src/commands/index.ts` Re-export new command
`packages/cli/src/index.ts` Wire `cmd === "export"` dispatch
`packages/cli/src/commands/help.ts` Help text
`packages/cli/package.json` `1.0.27` → `1.1.0`

Test plan

  • `bunx @biomejs/biome check src/` — clean
  • `bun test` — 2148 pass / same 4 pre-existing failures as upstream/main
  • 18 export tests pass: builders, script generation, claude-only gate, missing-connection rejection, deleted-spawn rejection
  • Manual: spawn claude on hetzner, do real work, run `spawn export`, push to a private repo, then `spawn claude hetzner --repo ` to verify the round-trip
  • Manual: confirm sanitizer strips token-shaped keys from `settings.json`

Followups

  • Claude-side introspection (MCPs / OAuth from session history) → spawn.md setup steps
  • Local cloud direct branch (skip SSH)
  • In-session `:export` slash command for triggering without leaving the agent

🤖 Generated with Claude Code

Adds `spawn export [name|id]` as a top-level subcommand. Captures a
running claude spawn into a redistributable github repo whose README
contains the canonical re-spawn command — the symmetric inverse of
`--repo`.

What gets exported:
- `~/project/`           working tree (with aggressive .gitignore)
- `~/.claude/`           sanitized agent system dir: skills, commands,
                         hooks, CLAUDE.md, AGENTS.md, settings.json
                         (with token-shaped fields stripped)
- `spawn.md`             generated re-spawn metadata
- `README.md`            generated; renders a re-auth checklist on github

The export runs over the existing SshRunner. v1 is claude-only; non-claude
agents return a clear "not yet supported" error. Bumps CLI 1.0.27 -> 1.1.0
because this is a real new surface, not a fix.

Followups (not in v1):
- claude introspects its own session (MCP servers, OAuth providers) and
  writes them into spawn.md's setup steps
- local cloud target uses a direct branch (currently routed through SSH)
- in-session `:export` slash command
@AhmedTMM AhmedTMM marked this pull request as ready for review May 1, 2026 07:31
AhmedTMM and others added 5 commits May 1, 2026 12:40
The update-check tests hardcode 1.0.x patch-bump scenarios as test fixtures
(e.g. mocking "latest" as 1.0.99 to verify patch auto-install policy). A
1.1.0 bump made those mocked versions look like downgrades and failed CI.
Realign with the team's recent patch-bump cadence — every recent feature
PR has shipped under 1.0.x.
…scan

Three changes per review feedback:

1. **Picker.** When the user has multiple exportable claude spawns and
   no positional target arg, show a clack `select` listing them. Auto-pick
   on a single match. Filter out non-claude / no-connection / deleted /
   sprite-console records up front.

2. **Claude decides the repo name.** No more slug prompt. The on-VM script
   runs `claude -p` with a name-suggestion prompt asking for a kebab-case
   project name (max 40 chars, [a-z0-9-]). Falls back to basename(~/project)
   then a timestamp slug if claude is unavailable or returns garbage.
   `gh api user --jq .login` provides the username; missing gh auth aborts
   with a structured JSON failure the CLI surfaces verbatim.

3. **Pre-commit secrets scan.** After `git add`, scan all staged files for
   known API-key shapes — Anthropic (sk-ant-api...), OpenRouter (sk-or-v1-),
   OpenAI (sk-proj-), GitHub PAT/OAuth/server (gh[ops]_), AWS (AKIA...),
   Hetzner (hcloud_), DigitalOcean (dop_v1_), and PEM private keys. Any
   match aborts the export with `{"ok":false,"error":"..."}` to the result
   file. The settings.json scrubber now recurses; previously it only
   stripped top-level + env keys.

Also expands the .gitignore deny-list with .spawnrc, .bash_history,
.aws/, .config/spawn/, .config/gcloud/, .gnupg/, *.token, *.credentials.

Bumps CLI 1.0.28 -> 1.0.29.
Exports are share-friendly artifacts — public by default makes the
'spawn link' (the printed re-spawn command) usable by anyone the user
hands it to. Override path stays via options.visibility for callers
that need private.

Bumps CLI 1.0.29 -> 1.0.30.
The printed spawn link is now:
  spawn claude <cloud> --repo <slug> --steps <list>

Source of the steps list:
1. Parse `--steps <value>` (or `--steps=<value>`) out of the original
   record.connection.launch_cmd.
2. Fall back to 'github,auto-update,security-scan' when the launch_cmd
   doesn't carry it (older spawns, or interactive launches that didn't
   pass the flag).

The respawn consumer reads SPAWN_ENABLED_STEPS from --steps and skips
the interactive setup picker entirely, so handing someone the spawn
link is a true zero-choice replay.

Adds parseStepsFromLaunchCmd + resolveSteps helpers, both exported
from the barrel for testability. README template grows a __STEPS__
placeholder; bash sed adds it to the substitution pass.

Bumps CLI 1.0.30 -> 1.0.31.
Review follow-ups to OpenRouterTeam#3377:

- Visibility: default private; interactive "make public?" confirm when
  the caller doesn't force one. Prior default-public + one-shot `gh repo
  create --push` was a public-leak footgun when the secret regex missed.
- `parseStepsFromLaunchCmd`: anchor both regexes to start-or-whitespace
  so `--no-steps=foo` no longer over-matches and returns `foo`.
- `--exclude=.git` on the claude/{skills,commands,hooks} rsync so a
  nested git checkout inside a skill doesn't leak its history.
- Replace `record!` / `conn!` non-null assertions with explicit
  narrowing — matches the project's type-safety rule (no `as`, no `!`).
- Tests: lock in private default, the .git exclude, and the negative
  `--no-steps=` regex case (14 new expects, 32/32 pass).
- Bump CLI to 1.0.32.
@la14-1
Copy link
Copy Markdown
Member

la14-1 commented May 2, 2026

Pushed review follow-ups to spawn-export (commit b96e643):

  • Visibility default flipped to private + interactive "Make the exported repo public on GitHub?" confirm when the caller doesn't force one. Matches the PR description and removes the public-leak footgun.
  • parseStepsFromLaunchCmd regex anchored to start-or-whitespace so --no-steps=foo can't over-match.
  • --exclude=.git on the claude-subdir rsync — a skill that's a git checkout no longer leaks its history into the export.
  • Removed record! / conn! non-null assertions in favour of explicit narrowing (project rule).
  • 3 new tests (32/32 pass, up from 18): locks in the private default, the .git exclude, and the negative --no-steps=foo case.
  • CLI bumped to 1.0.32.

Filed #3381 as a non-blocking follow-up to broaden the secret-scan regex (OpenRouter non-v1 prefixes, Slack, Stripe, Discord, GCP JSON blocks).

Biome clean, bun test 2170/0.

Copy link
Copy Markdown
Member

@la14-1 la14-1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed review fixes in b96e643. All CI green, approving to merge.

@la14-1 la14-1 merged commit 3e99bdd into OpenRouterTeam:main May 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants