Skip to main content

crossby

Write your AI-tool setup once. Reuse it, focus it, and carry it between coding agents.

crossby is a local interoperability layer for AI coding agents. You keep writing your rules, subagents, skills, permissions, hooks, and MCP servers in whatever format your main tool already uses — and crossby handles three jobs on top of that:

  • Reuse one configuration across compatible agents. One crossby sync translates your setup into each tool's native format, so a new CLI inherits it instead of starting blank. Direct sync targets are Claude, Cursor, Copilot, Codex, and Antigravity CLI.
  • Focus an agent on a task-shaped set of capabilities. A scene narrows the installed tools down to just the skills, agents, MCP servers, hooks, and permissions a task needs — persistently, or for a single launch.
  • Carry a live session to another agent. crossby handoff summarizes your current transcript and continues it in another tool, so you never re-explain what you were doing.
$ crossby sync --from claude

✓  rules         CLAUDE.md          →  AGENTS.md, .cursorrules, +1 more
✓  agents        .claude/agents/    →  .cursor/agents/, .codex/agents/, +2 more
✓  skills        .claude/skills/    →  .cursor/skills/, .agents/skills/, +2 more
✓  permissions                      →  translated for Cursor
✓  hooks                            →  written for Cursor, Codex, Copilot, Antigravity CLI
✓  mcp servers                      →  merged into Cursor, Codex, Copilot, Antigravity CLI

Any of the five direct-sync tools can be the source — crossby sync --from cursor works the same way. (No tool holds every surface, though: permissions live only in Claude's and Cursor's config, so a sync from Codex, Copilot, or Antigravity CLI has no permissions to read, and a sync to them writes none.) crossby is stateless by default — it reads directly from each tool's standard paths, so no config file is required.

Which workflow do you want?

I want to… Use Start with
Give my other tools the same rules/agents/skills/MCP/hooks I already wrote Sync crossby sync --plan --from claude
Run a tool with only the capabilities one task needs Scenes crossby scene list / crossby launch --scene <name>
Continue my current session in a different tool Handoff crossby handoff --from claude --to codex
Launch a tool and drive it from a browser instead of a shell Browser terminal crossby ui

Everything else in this README expands one of these. Jump to What crossby supports for the exact per-tool coverage.

Install

pip install crossby
# or
uv tool install crossby
# or
pipx install crossby

Requires Python 3.11+.

Quick start

Lead with the read-only inspection commands to see what crossby would do before it writes: sync --plan / --doctor, scene show, scene use --plan. Commands that write or launch (handoff, scene add / use) note their side effects in the sections below.

# Don't know where to start? Run crossby with no args for an interactive menu (TTY only).
crossby

1. Reuse your setup — sync

# See exactly what a sync would write, without touching any file.
crossby sync --plan --from claude

# Add a readiness rating and the post-sync validation checks it would run.
crossby sync --doctor --from claude

# Happy with the plan? Run it for real. Any direct-sync tool can be the source
# (claude, cursor, copilot, codex, antigravity-cli).
crossby sync --from claude

# Prefer to be walked through it? Omit --from for the interactive wizard.
crossby sync

--plan and --doctor never write; --dry-run runs a real sync in shadow mode. See Syncing configuration for what gets written and what stays additive.

2. Focus a session — scenes

# List the scenes defined in .crossby.yml, with per-concern counts.
crossby scene list

# No scenes yet? Drop in opinionated starters and tweak them.
crossby scene install-starters   # pr-review, deploy-watch, write-docs, presentation

# Preview what a scene resolves to per tool before applying it.
crossby scene show pr-review

# Apply it for one launch only — nothing tracked is touched, no cleanup needed.
crossby launch --scene pr-review --tool claude

Use crossby scene use <name> to persist a scene across tools, or crossby launch --scene <name> for a session-scoped one. See Scenes.

3. Continue elsewhere — handoff

# Write the handoff summary to .crossby/handoffs/ and stop — review it first.
crossby handoff --from cursor --to copilot --no-launch

# Or summarize the latest Claude session and continue it straight in Codex
# (this writes the handoff file and launches Codex with it pre-loaded).
crossby handoff --from claude --to codex

Handoff sources are the tools with readable transcripts (Claude, Cursor, Codex, Copilot). See Session handoff.

# A few more one-offs:
crossby launch ccyolo                       # launch a saved profile (see .crossby.yml)
crossby stats /path/to/transcript.txt       # parse a transcript for token usage
crossby convert "Bash(myapp:*)" --from claude --to cursor   # translate one allowlist pattern
crossby tools update                        # update your installed AI CLIs

Every command with missing arguments drops into a "Proceed / Change X" review, so you can accept the resolved defaults with one keystroke or tweak any single value before it runs.

What crossby supports

crossby drives eight tools, but not every tool does every job. The tables below are the source of truth for every support claim in this README.

Direct sync targets

A sync writer exists for five tools. These are the only tools crossby writes configuration into:

Surface Claude Cursor Copilot Codex Antigravity CLI
Rules (AGENTS.md ↔ CLAUDE.md ↔ .cursorrules ↔ Copilot) ✓ ✓ ✓ ✓ ✓
Agents (subagents) ✓ ✓ ✓ ✓ ✓
Skills ✓ ✓ ✓ ✓ ✓
MCP servers ✓ ✓ ✓ ✓ ✓
Hooks ✓ ✓ ✓ ✓ ✓
Permissions ✓ ✓ — — —

A checkmark is per surface, not per tool — the exceptions are real:

  • Permissions sync only to Claude and Cursor — the tools with a persistent per-project permission file that crossby writes. Copilot, Codex, and Antigravity CLI gate command permissions through launch-time flags or sandbox modes (--allow-tool, --dangerously-skip-permissions) rather than a synced policy file, so they have no permission writer. (Antigravity CLI's --sandbox is a terminal-restriction flag, not a permission grant, and crossby does not emit it.)
  • Plugins are detected, never written. .claude/plugins/, .claude/plugin-marketplaces.json, and .claude-plugin/marketplace.json are reported as Not Added; their bundled commands/agents/MCP servers must be migrated by hand.

Launch and handoff

Launching and handoff reach a wider set of tools than sync:

Tool Direct sync Launch Handoff source Handoff target
Claude ✓ ✓ ✓ ✓ auto
Cursor ✓ ✓ ✓ ✓ auto
Copilot ✓ ✓ ✓ ✓ auto
Codex ✓ ✓ ✓ ✓ auto
Antigravity CLI ✓ ✓ — ✓ auto
OpenCode — ✓ — ✓ auto
Antigravity IDE via Antigravity CLI ✓ (GUI) — manual
VS Code — ✓ (GUI) — manual
  • OpenCode and VS Code are launch adapters, not sync targets. crossby can launch them (and hand off to them), but neither has its own sync writer, so a sync never targets them directly. (One shared-file caveat: Copilot's MCP config lives at .vscode/mcp.json, so a sync to Copilot does write into that workspace file — as the Copilot target, not a VS Code one.)
  • The Antigravity IDE consumes the CLI's configuration transitively. It reads the same project-level .agents/ layout as Antigravity CLI (AGENTS.md, .agents/skills, .agents/agents, .agents/mcp_config.json), so syncing to antigravity-cli provisions the IDE too. There is no separate IDE sync target.
  • "✓ auto" vs "manual" handoff. For CLI targets, crossby launches the tool with the handoff summary pre-loaded as the initial prompt. GUI tools (VS Code, Antigravity IDE) can't take an initial message, so crossby writes the handoff file and prints its path for you to open by hand — it does not launch them with the context loaded.

Per-tool flag mappings and adapter internals live in CONTRIBUTING.md.

Syncing configuration

What gets synced

Config Strategy Notes
Rules Symlink (auto-copy) AGENTS.md ↔ CLAUDE.md ↔ .cursorrules ↔ .github/copilot-instructions.md (AGENTS.md is shared by Codex and Antigravity CLI). Falls back to copy with a <!-- crossby:manual-fix --> block when the source mentions surfaces specific to a different tool (/hooks, ExitPlanMode, permissionMode, …).
Agents Symlink / translate Markdown-shape tools (Claude / Cursor / Copilot / Antigravity CLI) symlink directories. Codex translates per file into .codex/agents/<name>.toml with permissionMode → sandbox_mode, model + effort family-mapped to GPT, lossy fields preserved as a manual-fix block.
Skills Symlink / translate All five tools accept the same SKILL.md shape, so symlink is the default. --strategy translate rewrites per tool with manual-fix notes for Claude allowed-tools on non-Claude targets, and converts Claude slash commands (.claude/commands/*.md) into claude-command-<slug> skills for every other tool.
Permissions Convert Canonical cmd:args ↔ Bash() / Shell() per tool — Claude and Cursor only (see above). Revocable: a pattern crossby wrote is removed when the source drops it (see Revocable sync).
Hooks Write Per-tool native hook schema; a crossby-written hook's matcher narrows as well as widens on re-runs, and the hook is revoked when the source drops it.
MCP servers Merge Source tool's MCP config → each target's; Authorization: Bearer ${VAR}, ${VAR} headers, and env-var self-references are rewritten into Codex bearer_token_env_var / env_http_headers / env_vars.
Plugins Detect (manual) .claude/plugins/, .claude/plugin-marketplaces.json, and .claude-plugin/marketplace.json are reported as Not Added; bundled commands/agents/MCP servers must be migrated by hand.

Preview before you write

crossby is built to be inspected before it touches a file:

  • crossby sync --plan shows a stage-by-concern dry-run summary and writes nothing.
  • --doctor adds a readiness rating (high / medium / low) plus the target-validation checks that would run afterward.
  • --validate-target re-parses already-synced files (TOML / JSON parseability, agent required fields, skill frontmatter, AGENTS.md size threshold, MCP command on PATH).
  • --dry-run runs a real sync in shadow mode.

After every real sync, the result table is also written to .crossby/sync-report.md — a portable | Status | Item | Notes | markdown table you can paste into a PR. A row can be Added, Removed (a revocation), Check before using (a lossy translation), or Not Added. Pass --no-persist-report to skip it, or --report-format markdown-table to render the same shape on stdout.

Translate strategy and manual-fix blocks

Default strategy is symlink (with content-aware copy fallback for rules). Pass --strategy translate to do per-file rewriting that preserves intent across tools whose semantics diverge:

crossby sync --from claude --strategy translate

When a field has no faithful equivalent on the target — e.g. Claude permissionMode: plan going to Codex, or allowed-tools going to a tool that doesn't enforce them — the rendered file gets a clearly-marked block:

<!-- crossby:manual-fix:start -->
## Manual migration required

- Claude-specific agent semantics carried over verbatim. The target tool does not enforce them — review and rewrite or remove as needed. Fields preserved: `permissionMode: plan`, `skills` preload list (`release-notes`).
<!-- crossby:manual-fix:end -->

Re-running crossby sync replaces the block in lockstep with the source — no stacking. Removing the block once you've addressed the note is supported; the next sync only re-emits it if the source still triggers it.

Need to translate a single allowlist pattern by hand? crossby convert "Bash(myapp:*)" --from claude --to cursor prints the equivalent pattern for the target tool. To translate a single subagent file (Claude / Cursor / Copilot / Codex), use crossby agents convert --from claude --to codex .claude/agents/researcher.md.

Revocable sync

crossby never removes configuration you own — it only ever revokes an entry it recorded writing, tracked in a per-machine, gitignored .crossby/owned.json ledger. A hand-authored entry that merely shares a name with a source entry is never revoked, though a normal same-named merge can still overwrite its contents — this guarantee bounds removal, not additive or update writes.

Within that boundary, hooks and permissions are additive by default but revocable: syncing --from A then --from B leaves each target reflecting B, not the union of both — a hook or permission pattern crossby wrote for A is taken back once it's gone from the source. MCP servers are narrower: crossby removes a server only when it wrote that server and the source marks it disabled (enabled: false); a server merely dropped from the source is left in place, so for MCP the two syncs leave the union. A same-named MCP server you wrote by hand is never deleted, though a normal MCP merge can still overwrite its config — the ledger bounds removal, not additive or update writes. A fresh clone starts with an empty ledger, so it never revokes until it has recorded writes of its own.

Scenes

A scene is a task-shaped slice of the project's skills, agents, MCP servers, hooks, and permissions. Activating one filters each installed tool down to just the selected capabilities, using the least-invasive mechanism per tool — a native disable key where one exists (Claude skillOverrides, MCP toggles), otherwise a re-pointed, filtered projection of the source directory.

# List the scenes defined in .crossby.yml, with per-concern counts
crossby scene list

# Show what a scene resolves to per tool, the mechanism each would use,
# and any selectors that matched nothing
crossby scene show pr-review

# Apply a scene to every installed tool
crossby scene use pr-review

# Preview without writing, or scope to one tool
crossby scene use pr-review --plan
crossby scene use pr-review --tool cursor

# Revert to the pre-scene baseline
crossby scene clear

# What's active, per-tool mechanism, and whether any managed file drifted
crossby scene status

Key behaviours:

  • Switching restores the true baseline. use B while A is active reverts A first, then applies B from the original pre-A state — so a later clear restores your settings, not A's (or B's) output. For each physical skills/agents target, .crossby/owned.json records whether it was absent, its exact literal symlink target, the exact Crossby-allocated backup holding a displaced real directory, or a canonical source that a scene deliberately left untouched. Shared targets such as .agents/skills have one baseline record. Clear and switch use that record directly; they never rediscover an original source or choose a backup by scanning .bak* names, so unrelated user backups stay untouched.
  • Reverting is ledger-driven. clear only undoes what crossby wrote (tracked in .crossby/owned.json); a skillOverrides, deny, or MCP-disabled entry you authored by hand is left untouched. Path-baseline records survive partial apply/clear failures and are removed only after the exact path is restored. clear works even after the active scene is renamed or deleted from .crossby.yml. One exception: a scene that narrows hooks or permissions removes those crossby-synced entries through the revocable-sync channel, and clear does not put them back — use warns when this happens, and clear repeats the warning before discarding its recovery state; re-run crossby sync to restore them.
  • Missing or corrupt recovery authority fails closed. If .crossby/owned.json can't be parsed as the expected structure — invalid JSON, a non-object root, a symlink, malformed scene tracking, an unsafe target/backup path, or an unknown descriptor — both use and clear, including --plan, refuse (exit 1) rather than revert from an empty view. Ordinary sync leaves the ledger bytes untouched and refuses hooks, permissions, and MCP writes before they can create ownership that cannot be persisted. An active PROJECT scene created by an older Crossby without a path-baseline record also refuses clear/switch with manual-recovery guidance; Crossby will not infer a source or claim a neighboring backup. (A valid ledger that simply owns nothing is fine, as is a genuinely absent one on a fresh per-machine clone.) Restore the ledger from backup or restore the applied paths/settings by hand; never delete it — a missing ledger reads as "crossby owns nothing," which re-opens the exact gap.
  • Drift is detected, not clobbered. status compares a per-tool content hash captured at apply time against the current file (normalised, so a semantically-neutral reformat is not flagged). use and clear refuse to revert a scene whose managed files have drifted — pass --force to proceed anyway.
  • --tool scopes list / show / use / clear / status (output for the read commands, effect for the write ones); --plan previews use / clear without writing.

Activation state is recorded in .crossby/scene-state.json (gitignored) — the active scene, when it was applied, the per-tool mechanism, and the drift hashes. It is bookkeeping for status; the authority for reverting is the ownership ledger.

Authoring scenes

You don't have to hand-write scenes: YAML. crossby scene create walks a wizard over the skills, agents, MCP servers, hooks, and permissions it actually finds in the project, and add / remove edit a scene's selectors from the command line:

# Interactive wizard — multi-select each concern, then a review step
crossby scene create pr-review

# Or build the exact same scene non-interactively (required when stdin is not a
# TTY — the wizard refuses rather than silently selecting everything)
crossby scene create pr-review \
  --skill "review-*" --skill knowledge --agent code-reviewer \
  --mcp github --exclude-mcp linear \
  --description "Review a pull request" --extends base --profile ccyolo

# Append to / remove from an existing scene's selectors (idempotent)
crossby scene add pr-review --permission "gh pr *"
crossby scene remove pr-review --exclude-mcp linear

# Print the scene block to stdout instead of writing it
crossby scene create pr-review --skill "review-*" --print

# Delete a scene (refused while it is active — clear it first, or --force)
crossby scene delete pr-review

# Drop in opinionated presets and tweak them
crossby scene install-starters   # pr-review, deploy-watch, write-docs, presentation

Every selector flag has an --exclude-* counterpart (--skill / --exclude-skill, and the same for --agent, --mcp, --hook, --permission). Adding a pattern to one channel removes it from the other, so include and exclude can never contradict — the move is reported when it happens.

Writes are surgical: only the edited scenes.<name> entry is rewritten, located by parsing the YAML rather than line-scanning. Everything outside that entry's span — every comment and every other section, including sibling scenes — is preserved byte-for-byte. Each write is backed up, re-parsed, and rolled back if it would produce an invalid config. Starter scenes skip any same-named scene you already have and are idempotent on re-run; because they use glob selectors, unmatched selectors warn rather than error.

Session-scoped scenes — crossby launch --scene

crossby scene use persists a scene into each tool's config files. When you instead want a scene to apply to one launch only, pass --scene to crossby launch. Tools with a launch-time lever (Claude, Codex ≥ 0.134.0, Copilot) use untracked launch artefacts and need no later clear. A terminal tool without one falls back to persistent activation; crossby warns, records successful or partial activation in scene status, and tells you to run scene clear afterward. A concern with no persistent mechanism remains a true no-op (and crossby warns that it was not narrowed), while a GUI tool launches without the scene. Narrowing can therefore be partial — see the per-tool table below.

Persistent launch fallback follows the same lifecycle as scene use: a shared skills directory expands only the recorded skills scope, active-scene switches cannot strand another tool, and corrupt ownership provenance fails closed before mutation. A scoped reapply checks every recorded path for the launch tool plus the shared skills path for a skills-only co-sharer; drift in an unrelated co-sharer concern is left untouched and does not block the launch. Launch has no --force escape hatch, so an applicable drift or another failed precondition aborts before the child process starts. Error rows are recorded as status: partial and launch continues with a prominent recovery warning; an apply exception or inability to record recoverable state aborts launch instead. If writing the state record fails after mutation, crossby reverts reversible changes, removes stale state for the rolled-back scope, and retains recovery state for any untouched active tools rather than start a child with untracked restrictions. Hook or permission removals cannot be restored by scene clear; clear repeats the crossby sync guidance before discarding their recovery record. An apply exception records completed removals and directs you to clear the scene, then run crossby sync. A state-write failure after such a removal directs you to run crossby sync, then fix the state path.

# Launch Claude with the pr-review scene for this session only.
# Emits --mcp-config <file> --strict-mcp-config and a --settings file, and
# writes nothing into .claude/ or .mcp.json.
crossby launch --scene pr-review --tool claude

# A scene may name a default profile; --scene alone picks it up.
crossby launch --scene pr-review

# Precedence is explicit flags > scene > profile > ai: defaults, so an explicit
# --profile or --model still wins over the scene's profile.
crossby launch --scene pr-review --tool codex --model gpt-6-sol

--scene selects exactly one launch tool (resolved from --tool, the scene's profile:, or ai.default_tool). A persistent fallback may additionally record another installed tool that shares the same physical capability directory (currently Codex and Antigravity CLI share .agents/skills), because re-pointing that directory necessarily affects both. Rendered session artefacts live under .crossby/scene/<name>/launch/, written atomically and kept out of git via .git/info/exclude. One exception: Codex's --profile reads only from $CODEX_HOME (usually ~/.codex, shared across projects), so its generated profile is written there as crossby-<project-slug>-<scene>.config.toml — namespaced by a project-root hash and carrying a generated-by header, so pruning stale profiles never touches a hand-written one. If that exact path contains a hand-written profile, crossby preserves it byte-for-byte and routes the launch through the recoverable persistent fallback instead.

Not every tool has a session-scoped lever. Where a tool can't scope a scene (or a specific concern) at launch, crossby warns rather than applying nothing silently — but the outcome varies: a CLI tool without a launch lever falls back to persistent activation, a concern with no lever at all can be left wide open, and a GUI tool just launches without the scene:

Tool Session-scoped lever
Claude --mcp-config <file> --strict-mcp-config, a --settings file of skillOverrides (needs claude ≥ 2.1.129), and --disallowedTools "Agent(<name>)" per deselected agent
Codex --profile <name> layering a generated $CODEX_HOME/<name>.config.toml (needs codex ≥ 0.134.0)
Copilot --disable-mcp-server <name> per deselected server; profile allow_tools approvals are rendered independently and entries naming an excluded MCP server are dropped
Cursor none — falls back to persistent activation (its only knob relocates the whole config base including auth)
OpenCode none — persistent fallback records the lifecycle but has no tool-config mechanism, so deselected servers stay enabled; scene clear removes the fallback state
Antigravity CLI none — falls back to persistent activation, warning that config was written
VS Code / Antigravity IDE none (GUI) — warns that the scene cannot apply, and launches without it

If a tool has a session lever but not for a concern the scene narrows (e.g. Codex scopes MCP but not agents), crossby warns and applies what it can.

Session handoff

# Hand off the latest session from the source tool
crossby handoff --from claude --to codex

# Or pick a specific session by id
crossby handoff --from claude --to codex --session-id 019cb497-ec14-7453-9224

# Write the handoff file but don't launch — review before switching tools
crossby handoff --from cursor --to copilot --no-launch

# Use the bundled Claude Code "compact" prompt instead of the default summary
crossby handoff --from claude --to codex --prompt-preset cc-compact

# Or supply your own summarization prompt (mutually exclusive with --prompt-preset)
crossby handoff --from claude --to codex --prompt ./my-prompt.md

crossby reads the chosen session from the source tool, asks an LLM to summarize it into a structured handoff document, writes it to .crossby/handoffs/HANDOFF-<timestamp>.md, and — for a CLI target — launches the tool with the file path (not its contents) as the initial prompt, so it fits under OS argv limits regardless of transcript size.

The default preset produces a structured six-section handoff (current task, key decisions, modified files, blockers, next steps, critical context). Pass --prompt-preset cc-compact to use Claude Code's partial-compaction prompt, or --prompt <path> to supply your own; both paths skip structured parsing and write the summarizer's output verbatim. The two flags are mutually exclusive.

Sources are the tools whose transcripts crossby can read: Claude, Cursor, Codex, Copilot. Targets are every supported tool — but the two GUI tools are a manual continuation path, not an automatic launch:

  • Claude, Cursor, Codex, Copilot, OpenCode, Antigravity CLI are launched with the handoff pre-loaded.
  • VS Code and the Antigravity IDE can't receive an initial message, so crossby writes the handoff file and prints its path for you to open by hand.

Launch options

crossby launch runs any supported tool with one unified set of flags. Ordinary autonomy options may degrade when a tool lacks an exact equivalent; native plan mode is stricter and fails before launch when Crossby cannot guarantee it.

Programmatic sandbox selection

Library consumers can choose sandbox confinement independently from autonomy by passing the keyword-only sandbox= argument to an adapter's launch(), build_launch_command(), or build_resume_command() method. This is an adapter API only; there is no crossby launch CLI flag or persisted config field for it.

Adapter sandbox=True (default) sandbox=False
Codex Preserves the existing conditional workspace-write composition described below --sandbox danger-full-access
Cursor --sandbox enabled --sandbox disabled
All others No sandbox-selection flag No sandbox-selection flag

The setting never changes approval behavior: Codex danger-full-access does not imply -a never, and yolo does not imply an unrestricted sandbox. Cursor now explicitly enables its sandbox on the default adapter path instead of inheriting a potentially disabled user setting. The static sandboxes_writes capability still describes an adapter's normal confinement; it is not a guarantee for a particular invocation made with sandbox=False.

Managed headless sessions

Library consumers have a typed boundary for an ordinary, non-Plan agent session, implemented by every terminal adapter:

from pathlib import Path

from crossby.ai_tools import (
    AbstractAITool,
    HeadlessInteractionMode,
    HeadlessNativeOutput,
    HeadlessSessionRequest,
    preflight_headless_session,
)

request = HeadlessSessionRequest(
    prompt="Implement the approved change",
    working_dir=Path.cwd(),
    native_output=HeadlessNativeOutput.TEXT,
    interaction_mode=HeadlessInteractionMode.UNATTENDED,
    timeout_seconds=600,
    idle_timeout_seconds=120,
)
adapter = AbstractAITool.get("codex")
if adapter.capabilities().supports_managed_headless_session:
    preflight = preflight_headless_session("codex", request)
    result = adapter.run_headless_session(request)

HeadlessSessionRequest, HeadlessSessionPreflight, HeadlessEvent, and HeadlessSessionResult are frozen models that reject unknown fields. Native text/JSON/JSONL selection is independent from an optional caller-provided JSON Schema. One absolute monotonic deadline covers validation, version probing, startup, callbacks, output parsing, and collection; idle and interaction timeouts can only shorten it. UNATTENDED fails an unexpected question and denies unresolved permissions. BROKERED requires an explicit interaction handler. Expected cancellation, timeout, native failure, and invalid output are terminal result statuses; unrecoverable spawn, protocol, or handler failures raise HeadlessTransportError with a bounded, prompt-free partial result. final_json_present distinguishes an explicit JSON null result from missing structured output.

Existing supports_headless / headless_flag command construction, interactive launch(), collected Plan sessions, and handoff summarization are unchanged.

Verified unattended support matrix

Every row was verified against the exact CLI build named in verified_version, which is also the runtime floor: an older install fails preflight with the adapter's remediation rather than running with a guessed contract.

Tool Verified build Native command Prompt Native outputs Response schema Unattended policy
Claude Code 2.1.263 claude --print stdin text, json, stream-json --json-schema --permission-prompts none denies anything that would prompt; permission_denials are reported
Codex CLI 0.154.0 codex exec --json stdin (-) text, jsonl --output-schema no approval channel exists in exec; the requested --sandbox and pinned network_access are the whole policy
Cursor 2026.09.10-fd3934a agent --print argument json, stream-json not supported --trust clears the workspace-trust gate; --force/--yolo is never granted, so edits can remain proposals
GitHub Copilot CLI 1.0.83 copilot --prompt -s argument text not supported --no-ask-user removes ask_user; --allow-all-tools is never emitted, so tools outside the session command policy are denied
OpenCode 1.18.31 opencode run --format json stdin text, jsonl not supported --auto is never emitted, so OpenCode's own noninteractive permission behavior stands
Antigravity CLI 1.2.6 agy --print argument text, json, stream-json --json-schema --print-timeout carries the session's overall Crossby deadline (it bounds the whole run, not one idle stretch); a native waiting state fails the session instead of stalling

Unattended behavior is uniform above the adapters. An unattended run never inherits parent stdin — a prompt is written to a pipe that is then closed, and every other adapter gets /dev/null. A failed or short prompt write is a transport failure, never an eligible native success. An unexpected native question fails the session; an unresolved permission is denied. A native terminal error, waiting state, or missing, repeated, or conflicting terminal evidence is never reported as success just because the exit status was 0. For framed output, Claude Code, Cursor, Antigravity CLI, and OpenCode reject every later frame after their terminal result or stop; Codex rejects every later typed frame. A timeout or cancellation kills the whole owned process group and returns a bounded, prompt-free result that keeps the session, thread, or conversation ID observed before the deadline. Native stderr is reported only as a fixed diagnostic, so caller-visible warnings cannot echo session content. Codex native error and failed-turn messages are likewise summarized without copying native text.

Only Claude Code, Codex CLI, and Antigravity CLI accept response_schema; the other three reject it before spawning anything. Claude and Antigravity also require JSON or streaming-JSON output for a schema, because that is the only wire on which each exposes structured output. With a schema, final_json is the validated structured output; otherwise TEXT returns the final response text and JSON/JSONL return the native object that carried it, exactly as the CLI emitted it. BROKERED sessions are not offered by any terminal adapter yet — none of these CLIs exposes a verified live question channel in its non-interactive mode. Cursor and Antigravity CLI encode effort in the model argument, so a headless request that specifies effort must also specify model. Cursor accepts the request only when the model encodes the requested tier exactly: matching [effort=…] overrides and registered bare-medium families are preserved, while auto, malformed or conflicting overrides, unknown models, and unavailable tiers fail before spawning. For Antigravity, the model must be a Gemini family with the requested native tier, and an already-suffixed ID must encode the same effort. Otherwise the request fails before spawning. With no separate effort, a suffixed Antigravity model must still be a known valid native tier (apart from the fixed provider ID gpt-oss-120b-medium); Crossby rejects a suffix it would otherwise normalize or drop rather than launch a different model.

docs/unattended-headless-verification.md records exactly what was probed on each build, including the two places where a live capture was unavailable and a documented contract was used instead.

Native plan mode

Native planning has two deliberately separate surfaces:

  • launch(..., plan_mode=True) enters native plan mode for a human and keeps the historical exit-code return value. supports_plan_mode is this activation-only compatibility view.
  • run_plan_session(PlanSessionRequest(...)) owns activation, interaction, exact-session collection, validation, and cleanup, then returns normalized Markdown plus provenance. supports_plan_session is true only when that full lifecycle is deterministic.

A positional prompt containing /plan is still ordinary text. Codex interactive startup instead submits the native slash command separately through its terminal. Plan mode, sandbox confinement, and approval policy are independent request dimensions; a collector either preserves a supported choice or rejects it. Static unsupported requests fail before spawning; native model-availability checks finish before the first planning prompt. Unknown and below-floor CLI versions also fail before a harness process starts. PlanSessionRequest rejects unknown fields instead of silently applying a default. An optional command_policy=PlanCommandPolicy(...) preauthorizes only the named commands for that collected session. It accepts exact simple commands and the existing canonical command:arguments:* form (a trailing * is also accepted); compound shell syntax, expansions, internal wildcards, blank entries, and duplicates fail Pydantic validation. Preauthorization suppresses permission prompts for matched operations. It is not a sandbox and does not claim that an unmatched command can never run.

Command policy, sandbox, network access, trusted directories, and approval policy compose independently. Sandbox/network select the execution environment; trusted_dirs extends the directories explicitly available to a collector and, for callback matching, the valid execution roots; command policy handles only command preauthorization; approval_policy controls unmatched permission requests. A match never grants file, network, MCP, or extra filesystem targets. Explicit conflicts fail instead of selecting a broader mode. tool-managed in the matrix describes native harness behavior, not guaranteed confinement.

Cursor and Antigravity CLI require an explicit model with explicit effort. Cursor opts into ACP's parameterized model picker, selects the exact advertised base model, applies the advertised reasoning-effort and optional thinking/fast variant options, and verifies all returned values before prompting; auto, unavailable models/tiers, and conflicting model-encoded tiers are rejected. Antigravity requires a compatible Gemini model whose native effort tier matches the request; missing, non-Gemini, unavailable, or conflicting model tiers are rejected before the collector launches. OpenCode collection accepts only low, medium, and high and passes the exact provider/model and variant to its native session API: interactive launches retain the legacy xhigh/max → high normalization, but a collected session rejects tiers the adapter cannot preserve exactly. OpenCode also requires that the selected model advertise the requested variant through its native provider API. Disabled or missing variants fail before prompting. With effort but no explicit model, Crossby uses the native plan agent's configured model, then the configured default model, and pins the model it validated. If neither is configured, supply model="provider/model"; Crossby does not guess from private recent-model state. Omitting effort keeps OpenCode's native default selection unchanged.

Support matrix (contracts verified against the listed builds through 2026-09-14):

Tool Native selector Collector / exact binding Interaction Sandbox / approval Command preauthorization Verified floor Remediation
Claude Code --permission-mode plan Interactive CLI; one .md in a fresh UUID plansDirectory Native terminal Tool-managed / tool-managed Native per-invocation --allowedTools; user/project/local settings sources excluded 2.1.263 Use a project-contained plan_output_dir; replaced directories/files are rejected, and partial artifacts are retained on failure
Codex CLI collaborationMode.mode = "plan" Headerless app-server JSONL; exact thread + turn + completed plan-item IDs, successful turn completion, and zero process exit Callback Preserved / preserved (on-request, never) Callback matching of authoritative simple commands; approve once only 0.153.4 Collection returns only after the bound turn completes successfully, its background terminals are cleaned, and app-server exits cleanly; interactive launch uses the separate terminal startup adapter described below
Cursor CLI ACP session/set_mode → plan ACP; configured model + thought level + thinking/fast state, exact session + blocking cursor/create_plan request ID + successful end_turn + zero process exit Callback, including separate final plan outcome Preserved / preserved (on-request, never) Unsupported; verified permission requests omit authoritative command input 2026.09.02-c22c1a3 Supply an ACP-advertised model and effort plus a handler for questions and the non-executing final outcome; remove explicit command policy or choose another collector
GitHub Copilot CLI Interactive --plan Collected sessions unsupported Native terminal for interactive launch only No collected-session policy Unsupported 1.0.83 (interactive activation) Use interactive launch(..., plan_mode=True) or another collector; the verified headless transport omits ask_user, so native question callbacks and continuation are unavailable
OpenCode Native session API agent="plan" Authenticated loopback server; fresh session ID + completed plan-message ID + export <exact-id> Live question/permission callbacks, including multi-select and separate plan approval Tool-managed / tool-managed Native session rules: catch-all ask, then scoped shell allows 1.18.29 Exported directories and terminal message must match the launched session; collection uses the native server because run --format json disables questions
Antigravity CLI --mode plan Headless JSON; case-insensitive terminal status + exact conversation ID + requested schema echo + structured_output.plan Resumable callback Tool-managed / tool-managed Unsupported; no authoritative permission channel 1.2.0 Free text and private brain storage are not artifact fallbacks; remove explicit command policy or choose another collector
VS Code Unsupported None None Unsupported Unsupported 1.136.1 Select plan mode manually or use a complete terminal collector
Antigravity IDE Unsupported None None Unsupported Unsupported — Select plan mode manually or use a complete terminal collector

tool-managed means the harness's native plan posture owns that dimension; only its safe default is accepted. preserved means Crossby enforces the listed caller choices explicitly; an unlisted approval policy is rejected before collection. Codex explicitly replaces configured extra writable roots with trusted_dirs, including an empty list, so ambient configuration cannot add unrequested directories. Copilot reports supports_plan_session=False and rejects collection before spawning, while its interactive native plan selector remains available. Protocol and resumable collectors never invent an answer or auto-approve implementation. A missing handler produces PlanInteractionRequiredError, including when the caller's stdin is a TTY. A caller that intentionally wants terminal input must explicitly pass the exported terminal_interaction_handler. Final plan approval is represented separately and an APPROVED response is refused by collectors where it would transition into execution. Native option lists are parsed without discarding malformed entries, and callback option IDs must match those exact choices; explicit denied, cancelled, or skipped outcomes take precedence over stale selections. Option-only permission callbacks reject free text before sending an approval. PlanInteraction.allow_other preserves a native free-form-answer affordance (including Codex isOther); callers must not return answer text when it is false.

Permission interactions may include interaction.operation. The typed operation separates authoritative argv from an authoritative shell_expression, and can also carry its execution directory, permission targets, operation kind, and native binding IDs. Missing fields stay missing: display prompts and labels are never parsed into executable evidence. Consequently, a missing or ambiguous operation cannot automatically match a callback-backed command policy. Compound shell expressions also fail closed rather than being split on whitespace or matched by string prefix.

Consumers that must decide before creating a future worktree can use the public preflight helper (or the equivalent adapter method):

from pathlib import Path

from crossby.ai_tools import (
    AbstractAITool,
    PlanCommandPolicy,
    PlanSessionRequest,
    preflight_plan_session,
)

future_worktree = Path.cwd() / ".future-plan-worktree"
request = PlanSessionRequest(
    prompt="Plan the change",
    working_dir=future_worktree,
    command_policy=PlanCommandPolicy(
        allowed_commands=("git status", "python:-m pytest:*"),
    ),
)
preflight = preflight_plan_session("codex", request, timeout_seconds=5)
print(preflight.detected_version, preflight.checked, preflight.deferred)

# Only after preflight succeeds does the consumer create/configure its worktree.
future_worktree.mkdir()
result = AbstractAITool.get("codex").run_plan_session(request)

Preflight validates complete-session capability, statically knowable request and command-policy compatibility, and a bounded exact CLI version against the adapter's published floor. It does not start a planning session or create files, directories, or configuration; timeout_seconds must be positive and finite. Its result explicitly lists what was checked and what remains deferred: filesystem state, authentication, model availability, protocol negotiation, and artifact collection. Runtime repeats static validation and version detection; callers must not treat a prior result as an authorization token or assume model/effort availability from static success.

The collected API is the automation surface:

from pathlib import Path

from crossby.ai_tools import (
    AbstractAITool,
    PlanCommandPolicy,
    PlanInteractionKind,
    PlanInteractionOutcome,
    PlanInteractionRequiredError,
    PlanInteractionResponse,
    PlanSessionError,
    PlanSessionRequest,
)


def answer(interaction):
    if interaction.kind is PlanInteractionKind.PLAN_APPROVAL:
        return PlanInteractionResponse(outcome=PlanInteractionOutcome.DENIED)
    # A real integration should obtain this from its user or workflow.
    if interaction.options:
        return PlanInteractionResponse(
            outcome=PlanInteractionOutcome.ANSWERED,
            option_id=interaction.options[0].option_id,
        )
    if interaction.allow_other:
        return PlanInteractionResponse(
            outcome=PlanInteractionOutcome.ANSWERED,
            answer="Use the existing public API",
        )
    return PlanInteractionResponse(outcome=PlanInteractionOutcome.SKIPPED)


adapter = AbstractAITool.get("codex")
try:
    result = adapter.run_plan_session(
        PlanSessionRequest(
            prompt="Plan issue #176",
            working_dir=Path.cwd(),
            command_policy=PlanCommandPolicy(allowed_commands=("git status",)),
        ),
        answer,
    )
except PlanInteractionRequiredError as exc:
    print(f"Waiting for {exc.interaction.question_id}: {exc.interaction.prompt}")
except PlanSessionError as exc:
    print(f"Plan collection failed: {exc}")
else:
    Path("PLAN.md").write_text(result.plan, encoding="utf-8")
    print(result.session_id, result.artifact_source, result.artifact_id)

Crossby returns Markdown and the available session, turn, item, conversation, or path evidence. It does not impose WADE validation and does not require the harness itself to create PLAN.md. timeout_seconds is one collector deadline shared by the version probe, initial invocation, question continuations, protocol waits, file-backed artifact collection, and subprocess-backed export. Caller-visible timeout errors omit subprocess command arguments because those arguments can contain prompts or continuation answers. Callback-based collectors also bound the time spent waiting for the interaction handler. Handlers run on daemon worker threads; timeout unwinds collection and closes the native process, and a late callback result is never forwarded. Python cannot forcibly stop caller code, so integrations must make external input waits cancellable or independently bounded and must not assume callback execution on the main thread. Claude's explicit native-terminal consent remains unchanged. Headless subprocess capture is also bounded to 8 MiB of stdout and 1 MiB of stderr. Captured, interactive, and protocol POSIX children use isolated process groups; cleanup kills remaining group members even if the server has already exited. Worker joins are bounded, and cleanup never closes a pipe while a reader or writer still holds its lock. Codex and Cursor also require a zero process exit after their successful protocol event before reporting success. Protocol writes share the session deadline. Each JSON-RPC frame is capped before queueing and an oversized frame terminates the owned protocol group. OpenCode interrupts status/header parsing at the absolute deadline and reads response bodies incrementally against that same deadline; like Claude file-backed plan artifacts, bodies are capped at 8 MiB. Claude anchors POSIX artifact reads to a directory handle opened before launch, validates file identity, and removes only empty run directories after failure. OpenCode selects the unique exported record matching the completed native message ID. Earlier progress and clarification messages are not competing plan artifacts; duplicate records of the completed message are rejected even when identical. Caller-supplied artifact-location failures remain PlanArtifactLocationError and are also caught by the collected API's PlanSessionError integration boundary.

For activation-only CLI use, continue calling adapter.launch(..., plan_mode=True) or adapter.build_launch_command(..., plan_mode=True) and handle PlanModeLaunchError. Interactive planning composes with native approval options listed below. Unsupported combinations fail before launch instead of replacing Plan mode or silently downgrading. --plan-output-dir <dir> remains a launch option only for Claude. The collected Claude API also accepts a project-contained plan_output_dir, but creates a unique run-owned child directory within it so a concurrent or newer artifact cannot be selected. Existing callers that omit command_policy keep the prior collected-session behavior. Ordinary launch(..., allowed_commands=...) also remains unchanged; new collected-session consumers should use PlanCommandPolicy, inspect capabilities().plan_mode.command_policy_support, and handle the exported PlanCommandPolicyUnsupportedError instead of translating private flags or writing persistent tool permissions.

When a Claude scene also narrows skills, Crossby combines plansDirectory and the scene's skillOverrides into one --settings JSON source. Claude treats repeated --settings occurrences as replacement, so emitting two would discard the requested plan destination.

Interactive planning and approvals

crossby launch --tool <tool> --plan --yolo opens the real terminal UI in native Plan mode on all six CLIs below. Planning and tool approvals are separate dimensions; Crossby uses native flags/settings, with a temporary terminal startup adapter for Codex.

CLI Native Plan selector With Crossby --yolo With Crossby --auto
Claude Code --permission-mode plan --allow-dangerously-skip-permissions --settings '{"useAutoModeDuringPlan":true}'
Cursor --mode plan --force --auto-review
GitHub Copilot --plan --yolo Unsupported
OpenCode --agent plan --auto Unsupported
Antigravity CLI --mode plan --dangerously-skip-permissions Unsupported
Codex CLI (POSIX) Terminal /plan, then the task -a never -a on-request -c approvals_reviewer='"auto_review"'

Verified startup versions: Claude 2.1.263, Cursor 2026.09.02-c22c1a3, Copilot 1.0.83, OpenCode 1.18.29, and Antigravity CLI 1.2.3. Codex terminal startup is verified on 0.154.0 and restricted to 0.154.x. GUI activation remains unsupported. capabilities().plan_mode.supported_launch_approval_modes declares the supported combinations; collected-session approval policies are independent and unchanged. These combinations are interactive-only (initial_message, not the builder's headless prompt).

Claude's ordinary --dangerously-skip-permissions replaces Plan mode, so its Plan launch uses --allow-dangerously-skip-permissions: the native interactive CLI keeps Plan selected and makes bypass available during planning. Native Plan instructions still guide the model; bypass is not filesystem confinement. Claude's classifier requires an eligible account/model; native policy and any useAutoModeDuringPlan: false setting can prevent classifier approval. Cursor's classifier is likewise controlled by its native account and organization policy. OpenCode calls its skip-approval flag --auto; Crossby maps it to --yolo, not to Crossby's classifier-based --auto. Explicit native denials still apply. Folder trust and plan/implementation decisions remain native user interactions.

Copilot additionally supports --plan --accept-edits through --allow-tool write, subject to its Plan restrictions. Codex supports it through -a on-request with its existing sandbox composition. Other CLIs reject that combination. In particular, Claude and Antigravity use one selector for Plan and accept-edits; Crossby never emits competing selectors. Multiple approval flags retain the usual yolo > auto > accept-edits precedence while keeping Plan selected.

To exercise a real interactive session through the public API, using your normal native authentication:

uv run python scripts/probe_native_plan.py --tool claude --approval yolo
uv run python scripts/probe_native_plan.py --tool cursor --model auto --approval yolo

The probe uses a temporary workspace and a read-only shell command, saves the launch argv and terminal transcript, and reports whether the command's unique output appeared. Inspect the native Plan/approval indicators and actual shell call; token presence alone is not proof of execution. Exit normally without approving implementation. See verification notes for the tested scope and remaining limits.

Autonomy modes

The remaining flags form the autonomy ladder (how much the agent may do without asking). They are permission modes, not model selection:

  • --accept-edits — auto-approve file edits, still prompt for shell/commands. Broadly portable (5 of the 6 CLIs support it at launch; OpenCode falls back to default prompting). (Codex is the exception — its accept-edits is sandbox-confined rather than per-command-prompted; see the note below the table.)
  • --auto — Classifier-mediated guarded autonomy, supported by Claude Code and Cursor CLI; on other tools it downgrades to that tool's accept-edits, then to default prompting — never to --yolo.
  • --yolo — skip all permission prompts.

Precedence (most permissive wins): yolo > auto > accept-edits. If you pass several of these three, the highest applies. A requested tier a tool doesn't support downgrades to the next lower autonomy tier it does support (emitting a UserWarning), stopping at default prompting — it never escalates. With --plan, only the explicitly supported native combinations above are accepted.

Per-tool mapping (Cursor updated September 2026; other entries verified July 2026; CLI flags can drift between versions, so treat the table as a point-in-time snapshot):

Tool --accept-edits --auto (classifier)
Claude --permission-mode acceptEdits --permission-mode auto
Codex -a on-request --sandbox workspace-write ↓ downgrades to accept-edits
Cursor CLI (none — its default Agent mode already is accept-edits) --auto-review
Copilot --allow-tool write ↓ downgrades to accept-edits
Antigravity CLI --mode accept-edits ↓ downgrades to accept-edits
OpenCode ↓ default prompting (config-only) ↓ default prompting
VS Code, Antigravity IDE ↓ default prompting (GUI) ↓ default prompting

Codex's old --approval-mode auto-edit was removed in the Rust CLI — crossby never emits it. Codex CLI 0.152 also removed the per-command untrusted approval policy, so Codex accept-edits maps to -a on-request (Codex's native "Auto" posture): the agent runs edits and commands freely inside the workspace-write sandbox and prompts you only before an action that would escape it (network, writes outside the workspace). Unlike the other tools, Codex's safety boundary here is the OS sandbox, not a per-command prompt — see the "Codex sandbox: linked worktrees & --network" section below. --approve-for-me is deliberately avoided (it would route even those escapes through automatic review with no prompt at all). Note Cursor CLI's default is accept-edits (the inverse of the Cursor IDE default), so --accept-edits is honored with no extra flag and no warning.

Cross-provider model translation

crossby launch translates model ids across families when the target tool wouldn't accept the source family natively:

# Pass a Claude model id to Codex — translated to gpt-6-luna under the hood
crossby launch --tool codex --model claude-sonnet-5 --effort high
# → codex --model gpt-6-luna -c model_reasoning_effort=xhigh

Sonnet shifts effort up one tier (low→medium, medium→high, high→xhigh) for coding-agent behavior. The reverse direction (gpt-6-sol → Claude) picks the lowest source tier so users don't accidentally over-bill. A UserWarning fires whenever a translation happens; pass a native id to silence it.

The GPT-6 targets need a current Codex CLI: with ChatGPT sign-in, Codex 0.154 rejects gpt-6-sol as unsupported, while 0.156.1 accepts it. Run npm i -g @openai/codex@latest if a translated launch fails that way.

Codex interactive Plan startup and events

Codex 0.154 has no native --plan launch flag. Crossby opens the real Codex UI in an inline terminal, submits /plan by itself, waits for its rendered Plan indicator, and then submits the initial message. Long prompts use bracketed paste after activation. The user then interacts with Codex normally, including native questions and approvals. No conversation text is turned into a plan file by this adapter.

This temporary adapter requires a POSIX terminal (macOS/Linux), Codex 0.154.x, and the main Python thread. It rejects headless/detached launches and unverified versions. Startup has a 120-second deadline; unexpected input, an unrecognized screen, or early exit fails without resubmitting the task. Trust/login screens remain interactive. Startup messages must be ordinary nonblank text, at most 100,000 UTF-8 bytes, without terminal control characters (newline/tab allowed).

Callers can pass prompt= as usual or own deferred delivery through the public startup callback:

from pathlib import Path
from crossby.ai_tools import (
    AbstractAITool, InteractiveLaunchEvent, InteractiveLaunchEventKind,
    InteractiveSession,
)

prompt = "Inspect this project and propose a plan."

def on_event(event: InteractiveLaunchEvent, session: InteractiveSession) -> None:
    if event.kind is InteractiveLaunchEventKind.PLAN_READY:
        session.send_message(prompt)

exit_code = AbstractAITool.get("codex").launch(
    Path.cwd(), plan_mode=True, on_event=on_event,
)

capabilities().plan_mode.supports_ready_event declares this optional API. PLAN_READY allows exactly one synchronous send_message() call; omitting it opens an empty Plan session. MESSAGE_SUBMITTED reports the observed start of the first native Plan turn. These are Crossby startup events derived from the terminal display, not Codex app-server events. Callbacks must return promptly and must not read/write the terminal. They cannot be combined with prompt=; subsequent conversation belongs to the native UI. build_launch_command() returns a Python module wrapper for this mode, so executing the built command performs the same startup handshake.

The app-server collected API remains available independently, with its existing 0.153.4 version floor and exact artifact binding. Terminal startup does not promise a custom plan-file destination. Plan --auto uses Codex's native approvals_reviewer="auto_review" setting; ordinary non-Plan auto behavior stays as documented above. Plan effort is applied to Codex's separate plan_mode_reasoning_effort setting as well as ordinary model effort.

Codex sandbox: linked worktrees & --network

Codex can confine writes with an OS sandbox (--sandbox workspace-write — Seatbelt on macOS, Landlock on Linux). With the default programmatic sandbox=True, crossby preserves its existing conditional composition. An explicit sandbox=False emits only --sandbox danger-full-access from the sandbox composer: no trusted or Git-metadata --add-dir roots and no workspace-write network pin. This remains independent from approvals—crossby never emits --dangerously-bypass-approvals-and-sandbox; Codex yolo is approval-skipping only (-a never), and approval never appears only when yolo is requested.

  • Linked worktrees & submodules just work when sandboxed. In a linked worktree the working tree's .git is a file pointing at metadata that lives outside the working directory, which the sandbox would otherwise block. crossby detects this and grants only the real git-metadata dirs outside the root to the sandbox with --add-dir — which adds to the writable roots, preserving any sandbox_workspace_write.writable_roots you configured — so sandboxed git operations succeed while the sandbox stays on. A normal checkout grants nothing. This applies to launch, --resume (approval-neutral: no -a injected), and the headless handoff summarizer.
  • --network (Codex only). crossby launch --network allows network access inside the sandbox (package installs, remote fetch/push). It is security-sensitive and off by default. On tools without a sandbox network opt-in it is warned and ignored on every path (launch, resume, GUI).
  • Explicit network pin. Whenever crossby forces workspace-write (a worktree, --network, --accept-edits, or --trusted-dir), it also emits an explicit -c sandbox_workspace_write.network_access=<true|false> (true only with --network) so an ambient network_access = true in your Codex config can never silently enable networking in a crossby-managed sandbox. A plain, unmanaged launch emits no sandbox flag and stays byte-identical.

Browser terminal — crossby ui

Run an AI tool from a web page instead of a shell:

crossby ui                 # serve the current directory, open a browser
crossby ui --path ~/work/api --port 7420 --no-open

Pick a tool, model, effort, autonomy and initial message in the form, hit Launch, and the tool appears in an embedded terminal. It is a real terminal, not a log view: the tool runs on a server-side pseudo-terminal, so its full-screen interface, colours, keybindings, Ctrl-C and resize behaviour all work exactly as they do in your shell.

Multiple sessions, in tabs. Launch again for another session — each gets its own tab, its own terminal and its own PTY, and they run side by side. Every session streams over a single connection, so the tab count is not limited by the browser's per-origin connection cap.

Each session picks its own folder. The Folder control browses the directories the server allows and launches the tool there, so one UI can drive several projects at once. When sessions span more than one folder, the tabs say which is which. Your last choice is remembered between visits.

By default that is --path and everything beneath it. --allow-dir adds more trees:

crossby ui --path ~/work --allow-dir ~/oss --allow-dir ~/scratch

Only the operator sets those roots. The page browses and launches within them and cannot reach outside — a request for anything else is refused, symlinks included, because containment is checked against the resolved path.

Reloading the page does not lose your work. Sessions live on the server, so a refresh reattaches to everything still running and repaints each tab from its scrollback.

Stop vs close. Stop this session ends the tool but keeps the tab so you can read the final output; the tab's × closes it (stopping it first if it is still running).

The form is generated from each adapter's declared capabilities, so a tool only ever offers what it actually supports — no effort selector on Copilot, no YOLO rung on OpenCode, no plan mode on Codex, and the sandbox and network controls only where a tool honours them.

Autonomy is one choice rather than a row of checkboxes, matching crossby launch: plan mode is exclusive, and the rest form a ladder.

Rung What the tool may do
Ask before acting Prompts for edits and commands (default).
Plan only Native plan mode — proposes, changes nothing.
Auto-accept edits Edits apply without asking; commands still prompt.
Auto The tool's own classifier decides what needs asking (Claude only).
YOLO No permission prompts at all.

Closing a tab asks first when the session is still running — the × sits next to the label, and ending a live tool by a stray click is not a good trade. Tabs of the same tool are numbered so two Claude sessions are distinguishable.

Switching tabs: Cmd/Ctrl + 1–9 (9 is the last tab). Each tab shows its number.

Cmd is the right modifier for a terminal UI: macOS never delivers it to the tool, so it cannot collide with the tool's own bindings the way Ctrl- and Option- would. This is confirmed working in ordinary Chrome on macOS.

Some browsers do claim Cmd/Ctrl + a digit for their own tab strip, so a second binding is registered for those: Cmd/Ctrl + Alt + 1–9, which nothing reserves. Use whichever your browser leaves free.

Installing the page as an app window (Chrome ▸ Cast, save and share ▸ Install page as app) drops the browser chrome and removes any doubt, which is worth doing if you use the UI regularly.

Scope in this release. The UI launches sessions and lets you interact with them. Scene selection, profiles, resume and transcript capture are not wired into it yet; use the CLI for those.

Security

The server spawns AI tools with access to your filesystem, so it is locked down by default:

Control Behaviour
Binding Loopback only. A non-loopback --host is refused outright.
Token The printed URL carries a random access token; every request needs it. Treat the URL as a secret.
DNS rebinding Requests whose Host header is not loopback are rejected.
Cross-origin Any request carrying a foreign Origin is rejected.
Static assets The page shell (HTML/CSS/JS) is served unauthenticated — it holds no secrets and does nothing without a token — but still only same-origin.
Working directory Sessions run only at or below --path and any --allow-dir, checked against the resolved path so symlinks cannot escape.
Concurrency Capped at 16 live sessions per server.

There is no multi-user mode, no remote access, and no authentication beyond the token. Do not expose it through a tunnel or reverse proxy.

Platform. Requires POSIX pseudo-terminal support (macOS, Linux, WSL). Windows would need a ConPTY backend, which crossby does not ship yet — the command fails with a clear message rather than degrading silently.

What has been verified

Verified against the real CLIs — Claude Code and Codex, signed in as usual — covering startup, trust prompts driven by arrow keys, streamed answers with syntax highlighting, Ctrl-C interrupting a reply, resize with the tool re-wrapping its output, and clean exit.

Also exercised against real full-screen TUIs (vim, top, less) for alternate-screen rendering, self-driven repaint, mouse reporting, and resize confirmed against the running program's own columns/lines.

Not yet exercised: first-run sign-in. Interactive login, OAuth browser redirects and device-code prompts have not been tested through the browser terminal. Authenticate the tool once in a normal shell first. If a tool opens a browser during login, it opens on the machine running the server.

Reattaching forces a redraw. Replayed scrollback is a cushion, not a transcript: a tool with an idle animation (Codex emits ~10.8 KB/s doing nothing) pushes real output out of the buffer within seconds. On reattach the page nudges the terminal size, which makes the tool repaint from its own state.

Update installed tools

Keep your AI CLIs current without remembering each tool's own updater (claude update, codex update, agent update, agy update, copilot update, opencode upgrade, …):

# Pick which installed tools to update (default all), then run each updater
crossby tools update

# Update specific tools only
crossby tools update --tool claude --tool codex

# See the resolved command per tool without running anything
crossby tools update --dry-run

# Skip crossby's confirmation prompt (e.g. in a script)
crossby tools update --yes

crossby lists the installed, updatable tools, runs each tool's own updater sequentially — continuing past any failure — and prints a report of Tool · Version (before → after) · Status (updated / version unchanged / ✓ / ✗).

v1 limitations. Each tool declares one static update command; there is no detection of the install method (npm / brew / standalone). A tool that updates a different installation than the one on PATH can report success without changing the active version (surfaced as a "version did not change" warning). GUI tools (the Antigravity IDE, VS Code) self-update through their IDE and are never offered. This updates the managed AI tools, not the crossby CLI itself.

Optional: .crossby.yml

crossby is stateless by default — crossby sync reads directly from each tool's standard paths and needs no config file. Add a .crossby.yml only when you want saved profiles, per-tier model defaults, scenes, or command defaults. Run crossby init to scaffold it interactively, or hand-author it:

version: 1
ai:
  default_tool: claude
  default_model: claude-sonnet-4.6
  effort: medium

models:                           # per-tool, per-complexity-tier overrides
  claude:
    easy: claude-haiku-4.5        # `crossby launch --complexity easy`
    complex: claude-sonnet-4.6    # `crossby launch --complexity complex`
    complex_effort: high          # …and raise effort to `high` for that tier
    very_complex_effort: xhigh
  codex:
    complex: gpt-6-sol
    complex_effort: xhigh

profiles:
  ccyolo:                         # → crossby launch ccyolo
    tool: claude
    model: claude-sonnet-4.6
    effort: high
    yolo: true
  quick:                          # → crossby launch quick
    tool: cursor
    model: haiku
    effort: low
  cop:                            # → crossby launch cop
    tool: copilot
    allow_tools:                  # Copilot-native approval entries
      - shell(git:*)
      - github
      - github(create_issue)

scenes:                           # task-shaped bundles of capabilities
  base:
    skills:
      exclude: [deploy-*]
  pr-review:
    description: Review a pull request
    extends: base                 # single-parent composition
    profile: ccyolo               # default launch profile for this scene
    skills:
      include: [review-*, knowledge]
    agents:
      include: [code-reviewer]
    mcp:
      include: [github]
    hooks:
      include: ["pre_tool_use:*"]
    permissions:
      include: ["git diff:*", "gh pr *"]

sync_defaults:                    # fed into `crossby sync`
  from: claude
  to: cursor

handoff_defaults:                 # fed into `crossby handoff`
  from: claude
  to: codex
  prompt_preset: default
  token_budget: 32000

Profiles are named bundles of --tool / --model / --effort / --accept-edits / --auto / --yolo, plus Copilot-only allow_tools. Run them by name (crossby launch ccyolo) or with --profile ccyolo. Explicit flags on the command line still override the profile.

Copilot profiles may also set allow_tools. Each value uses Copilot's native --allow-tool syntax and is passed through unchanged on every Copilot launch, with or without --scene; it is not Crossby's canonical command:arguments permission format. A scene only filters entries that name MCP servers it excludes, so shell(git:*) remains independent of scene visibility.

The models: section maps a tool + complexity tier to a model id. Each tier (easy / medium / complex / very_complex) also takes an optional <tier>_effort override. Effort resolution order is --effort flag → CROSSBY_EFFORT env → per-command ai.<command>.effort → per-tier <tier>_effort → global ai.effort. Values must be one of low / medium / high / xhigh / max.

sync_defaults and handoff_defaults feed the interactive prompts for those commands — CLI flags still win, and you always get the "Proceed / Change X" review before anything runs.

Agent-readable runbook

crossby init --install-skill copies the bundled crossby-sync skill into every installed tool's skills directory. From inside Claude Code / Codex / Cursor / etc., the LLM can drive the full sync loop end-to-end — scan, plan, fix manual-fix blocks, validate — without leaving the session. The bundle is at src/crossby/data/skill/; its references/differences.md file has the per-surface mapping table.

The bundle follows the Agent Skills standard layout (SKILL.md, agents/openai.yaml, references/), so Codex users can also install it via the upstream $skill-installer skill:

$skill-installer install https://github.com/ivanviragine/crossby/tree/main/src/crossby/data/skill

That installs it globally under $CODEX_HOME/skills/ instead of per-project. Use whichever fits — crossby init --install-skill for a project-scoped install that travels with the repo, or $skill-installer for a one-time user-scoped install.

Documentation

  • CONTRIBUTING.md — architecture, how to add a new tool, per-tool flag reference, release process.

Contributing

Issues and PRs welcome. See CONTRIBUTING.md for development setup and architecture.

License

MIT

Release files for crossby 0.37.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for crossby 0.37.0
File Size Uploaded
crossby-0.37.0.tar.gz 1.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for crossby 0.37.0
File Interpreter ABI Platform
crossby-0.37.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.8 MB

Release files / crossby-0.37.0.tar.gz

Download URL crossby-0.37.0.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
200667fb1810db11df325bff8cab561175d21efc8d69ca69877f7b2ea8767b37
BLAKE2b-256 checksum
How to use checksums
bb75e89b6ea476f2027818039e611e93c4947e1321256feca8c7212d2aab9eb6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / crossby-0.37.0-py3-none-any.whl

Download URL crossby-0.37.0-py3-none-any.whl
Size 686.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5946d6c8366e38319cf61cc3d04089085852d6fe34e5f89aae70f705521a2ca5
BLAKE2b-256 checksum
How to use checksums
d68abfc5ab25e30e541c5d26befa1c6bd2b2565f1376e7defd3c87bec87f2ba5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

0.37.1

2 release files

This release

0.37.0 This release

2 release files

0.36.1

2 release files

0.36.0

2 release files

0.35.0

2 release files

0.34.0

2 release files

0.33.0

2 release files

0.32.1

2 release files

0.32.0

2 release files

0.31.0

2 release files

0.30.2

2 release files

0.30.1

2 release files

0.30.0

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.4

2 release files

0.24.3

2 release files

0.24.2

2 release files

0.24.1

2 release files

0.24.0

2 release files

0.23.7

2 release files

0.23.6

2 release files

0.23.5

2 release files

0.23.4

2 release files

0.23.3

2 release files

0.23.2

2 release files

0.23.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.4

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page