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 synctranslates 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 handoffsummarizes 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--sandboxis 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.jsonare reported asNot 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 toantigravity-cliprovisions 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 --planshows a stage-by-concern dry-run summary and writes nothing.--doctoradds a readiness rating (high/medium/low) plus the target-validation checks that would run afterward.--validate-targetre-parses already-synced files (TOML / JSON parseability, agent required fields, skill frontmatter,AGENTS.mdsize threshold, MCPcommandonPATH).--dry-runruns 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 cursorprints the equivalent pattern for the target tool. To translate a single subagent file (Claude / Cursor / Copilot / Codex), usecrossby 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 BwhileAis active revertsAfirst, then appliesBfrom the original pre-Astate — so a laterclearrestores your settings, notA's (orB's) output. For each physical skills/agents target,.crossby/owned.jsonrecords 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/skillshave 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.
clearonly undoes what crossby wrote (tracked in.crossby/owned.json); askillOverrides,deny, or MCP-disabledentry 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.clearworks 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, andcleardoes not put them back —usewarns when this happens, andclearrepeats the warning before discarding its recovery state; re-runcrossby syncto restore them. - Missing or corrupt recovery authority fails closed. If
.crossby/owned.jsoncan'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 — bothuseandclear, including--plan, refuse (exit 1) rather than revert from an empty view. Ordinarysyncleaves 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.
statuscompares a per-tool content hash captured at apply time against the current file (normalised, so a semantically-neutral reformat is not flagged).useandclearrefuse to revert a scene whose managed files have drifted — pass--forceto proceed anyway. --toolscopeslist/show/use/clear/status(output for the read commands, effect for the write ones);--planpreviewsuse/clearwithout 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_modeis 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_sessionis 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 0.157.0, and restricted to
0.154.x and 0.157.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-editwas removed in the Rust CLI — crossby never emits it. Codex CLI 0.152 also removed the per-commanduntrustedapproval policy, so Codex accept-edits maps to-a on-request(Codex's native "Auto" posture): the agent runs edits and commands freely inside theworkspace-writesandbox 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-meis 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-editsis 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 and 0.157 have 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 or 0.157.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
.gitis 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 anysandbox_workspace_write.writable_rootsyou configured — so sandboxed git operations succeed while the sandbox stays on. A normal checkout grants nothing. This applies to launch,--resume(approval-neutral: no-ainjected), and the headless handoff summarizer. --network(Codex only).crossby launch --networkallows 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>(trueonly with--network) so an ambientnetwork_access = truein 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.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| crossby-0.37.2.tar.gz | 1.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| crossby-0.37.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.8 MB
Release files / crossby-0.37.2.tar.gz
| Download URL | crossby-0.37.2.tar.gz |
|---|---|
| Size | 1.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e90e39338806dcc6af388fe9baf3ea6914085df29ce4c034a7533795447a23f
|
|
BLAKE2b-256 checksum How to use checksums |
7a0b323d9d9ba7a650e72d75272550f51c109a51c44f9332ad57dee259811b4f
|
| 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 logRelease files / crossby-0.37.2-py3-none-any.whl
| Download URL | crossby-0.37.2-py3-none-any.whl |
|---|---|
| Size | 686.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5aa4717c65c66678059f6e521fac4ddd2441f7d2fd59fa26d9fe03c1f3493e53
|
|
BLAKE2b-256 checksum How to use checksums |
edb4f7a479890ea2dd6b3119c5a8eb93f82d4324073a0eda6471c846e2bfa69c
|
| 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