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 |
Everything else in this README expands one of these three. 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-5.2
--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.
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.
Neither surface treats prompt text such as /plan as activation. 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; ordinary interactive launch has no pre-prompt selector and remains activation-only unsupported |
| 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. --plan remains mutually exclusive with
--yolo, --auto, and --accept-edits. --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.
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— Claude Code's classifier-mediated guarded autonomy (a separate model reviews each non-read action). Claude-only among the CLIs crossby drives; 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. None can be
combined with --plan.
Per-tool mapping (verified against official docs, 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) | ↓ downgrades to accept-edits |
| 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-5.4-mini under the hood
crossby launch --tool codex --model claude-sonnet-4.6 --effort high
# → codex --model gpt-5.4-mini -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-5.4 → 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.
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.
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-5.4
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.32.0
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.32.0.tar.gz | 864.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| crossby-0.32.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / crossby-0.32.0.tar.gz
| Download URL | crossby-0.32.0.tar.gz |
|---|---|
| Size | 864.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
38d352613414e39cd6d3bd576c22d398e22672ee2a20b10d80ab3ac724ed5a81
|
|
BLAKE2b-256 checksum How to use checksums |
16c36f0bf276c4d414d0b93d7fccb37b50cbc8aab231ed73bcc8cfe3fc3d5cb3
|
| 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 15, 2026.
Transparency logRelease files / crossby-0.32.0-py3-none-any.whl
| Download URL | crossby-0.32.0-py3-none-any.whl |
|---|---|
| Size | 507.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
73f379044fedd4baae80e37ecd6e6ca29220c87d68812fc212dc5fe4619aeddc
|
|
BLAKE2b-256 checksum How to use checksums |
0f81a58c68fcaf3099ab5cfd2a7c695d1f32a8edd249b32de1da4a3a25f47fb6
|
| 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 15, 2026.
Transparency log