team

Cross-host portability

What this is. A portability study: how Team’s Claude Code plugin primitives map onto Gemini CLI and Codex CLI, and the strategy chosen to support all three hosts. It is a decision document, not a code change. The source issue is #50. It is consumed by the #56 Gemini port epic and the #57 Codex port epic, which execute against the matrix, the gap analysis, and the “what #56/#57 execute against” section below.

Contents

Current state

Team is a Claude Code-native plugin. It ships 13 agents (agents/*.md), 51 skills (skills/*/SKILL.md + registry.json), and 4 hooks (hooks/*.mjs), registered through .claude-plugin/plugin.json. The orchestrator walks the QRSPI phase table (skills/team/SKILL.md), persisting state as artifact files under docs/plans/<id>/ and coordinating agents via the Task tool and SendMessage resume.

The portability surface splits cleanly. Already host-neutral: the Markdown bodies of every agent and skill (plain prose, no Claude Code APIs); the .mjs hook logic (Node stdlib only, so node:fs/promises, node:child_process, node:path, node:url, and zero npm deps); the artifact file I/O under docs/plans/<id>/; and the agent→orchestrator JSON-envelope convention. These layers move to any host unchanged.

The portability-blocking surface is the set of Claude Code-specific contracts. There are four non-portable bindings:

  1. Hook event names and the stdin/stdout JSON contract. The stdin schema (tool_name, tool_input, cwd), the stdout/stderr envelope (hookSpecificOutput.{permissionDecision, additionalContext}, systemMessage), and exit-code semantics.
  2. Host path env vars. ${CLAUDE_PLUGIN_ROOT}, interpolated into every hook command (plugin.json:18,30,41,52), and CLAUDE_PROJECT_DIR, read from the environment inside the hook bodies (pre-compact-anchor.mjs:27, session-start-recover.mjs:31, post-write-validate.mjs:103). The two use different mechanisms: manifest interpolation vs. runtime env lookup.
  3. Agent/Task tool dispatch, plus SendMessage resume and depth/parallel nesting semantics.
  4. SKILL.md slash-command auto-registration, plus user-invocable.

Agent frontmatter field semantics (name/model/tools/skills/permissionMode) are also host-interpreted. Everything portable rides on top of these four non-portable bindings. In particular the model: field is a Claude-specific model name; making it portable means resolving it through host-neutral config rather than baking a literal into each definition (see .team/config.json under Desired end state).

Desired end state

A single canonical “core” of host-neutral definitions, meaning the Markdown bodies and the Node hook logic, maintained once, plus thin per-host binding shims that translate the four blocking contracts into each host’s idiom. Claude Code keeps its current .claude-plugin/plugin.json + skills: injection. A Gemini build emits .gemini/ (settings.json hooks, agents/*.md, skills/*/SKILL.md, TOML commands). A Codex build emits .codex/ (config.toml/hooks.json, agents/*.md, .agents/skills/). The high-churn binding layer is isolated from the stable cores, so a host API change touches one shim, not 68 definition files.

Per-project configuration is host-neutral. Each project Team is configured in carries one .team/config.json at its root: plain JSON, part of the portable core, identical on every host. It declares the settings that would otherwise leak host specifics into the definitions, namely the map from Team’s abstract model tiers to the active host’s concrete model IDs (the agent model: frontmatter becomes a tier key, not a literal Claude model name), host selection, per-host parallelism caps, and the multi-repo list. The per-host shims read it; they never redefine it. It is the host-neutral counterpart to the per-host manifests: those carry only bindings, .team/config.json carries the host-agnostic project config.

This document does not build that. It is the strategy and capability matrix that lets epics #56 and #57 build it, each targeting full parity (all four hook events, parallel + nested subagents, structured returns) against named, tracked host risks.

Patterns to follow

The capability matrix

Team primitive × host. Each cell is either native (direct host equivalent), workaround (achievable via a documented alternate mechanism), or hard gap (no host facility, so it has to be designed around).

Team primitive Claude Code Gemini CLI Codex CLI
Agent/skill Markdown bodies native (loaded as-is) native (loaded as-is) native (system-prompt body)
Custom slash entry points native (SKILL.md auto-register) native (TOML in .gemini/commands/) native (built-ins + Skills; prompts deprecated→Skills)
On-demand SKILL.md injection native (skills: + auto-load) native (.gemini/skills/SKILL.md, progressive disclosure via activate_skill) native (.agents/skills/SKILL.md, description-matched implicit invocation)
Subagent dispatch (parallel) native (Agent/Task tool) native (.gemini/agents/*.md, parallel) native (spawn_agent/wait_agent…, features.multi_agent)
Nested subagents native (depth 2, ≤4, read-only) workaround: parallel yes, but subagents cannot spawn subagents workaround: max_depth=1, nesting capped one level
Structured agent→caller output native (final-text JSON envelope) native at CLI (--output-format json/JSONL, shipped via gemini-cli#8022); subagent-return boundary under-specified (unverified) native and strongest (--output-schema JSON Schema); a silent-drop bug under tools (codex#15451) was fixed April 2026
PreToolUse hook native native (BeforeTool) native (PreToolUse)
PostToolUse hook native native (AfterTool) native (PostToolUse)
SessionStart hook native native (SessionStart) native (SessionStart)
PreCompact hook native native (PreCompress) native (PreCompact, + PostCompact)
Hook stdin/stdout JSON contract native (Claude schema) workaround: own schema (hook_event_name, decision, exit 2), fields remap workaround: own schema, mirrors Claude closely (permissionDecision:"deny"/exit 2)
Plugin-root / project-dir env vars native (${CLAUDE_PLUGIN_ROOT}, CLAUDE_PROJECT_DIR) workaround: no equivalent, so pass paths via hook config, argv, or stdin workaround: no equivalent, so resolve via .codex/ trust + config
Always-on project context native (CLAUDE.md) native (GEMINI.md) native (AGENTS.md)
MCP tools native native (stdio/SSE/HTTP, OAuth) native (stdio/HTTP, OAuth, per-tool approval)
MCP prompts-as-slash-commands native native (/prompt-name --arg) hard gap: MCP prompts unsupported client-side, so route via Skills
MCP resources native native (@server://path) native (read_mcp_resource/list_mcp_resources)
Manifest / binding format .claude-plugin/plugin.json .gemini/settings.json + commands TOML config.toml/hooks.json + .codex/
Per-project config (host-neutral) .team/config.json (plain JSON, read by portable core) .team/config.json (same file, unchanged) .team/config.json (same file, unchanged)
Abstract model tier → host model native (model: is a literal Claude model) workaround: resolve tier via .team/config.json map workaround: resolve tier via .team/config.json map

Reading the matrix: every row that Team’s behavior depends on is native or workaround on both hosts. There is no hook-event gap. All four events map natively, and on-demand skills, subagents, MCP tools, and MCP resources are native on all three hosts. The one remaining hard gap is narrow: Codex does not surface MCP prompts as slash commands, though its MCP tools and resources are fine, and it has a clean detour (route slash entry through Codex Skills, below).

The landscape is recent, though not uniformly so. As of mid-2026 both Gemini CLI and Codex CLI ship full hooks systems, parallel subagents, custom slash commands, on-demand skills, MCP, and structured headless output. Earlier (2025) write-ups that treated these as hard gaps are stale. Maturity differs. Gemini’s extension surface has been stable since late 2025 (hooks around December 2025, subagents around August 2025; latest v0.49.0), while Codex’s hooks and multi-agent are younger, rolled out March to May 2026 across v0.114-v0.129 (latest v0.142.3). See the recency risk in the gap analysis.

Gap analysis

After verifying every capability against the host repos (2026-06-27), the gap picture is narrower than the earlier draft assumed. One hard gap remains, plus a cross-cutting recency caveat:

  1. Codex does not expose MCP prompts as slash commands (hard gap). Codex MCP supports tools and resources (read_mcp_resource/list_mcp_resources) but not MCP prompts, so “MCP-prompts-as-slash-commands” works on Gemini and not Codex. The workaround for #57 is to route every slash-style entry point through Codex Skills (the documented successor to deprecated custom prompts), not MCP. This is why the chosen strategy does not depend on MCP (decision 4).

  2. Recency risk, Codex-weighted. This is cross-cutting rather than a primitive gap. Gemini’s extension surface is mature (hooks around December 2025 in v0.20-0.21, subagents around August 2025, latest v0.49.0) and is a low contract-churn risk. Codex’s hooks and multi-agent are younger, rolled out March to May 2026 across v0.114-v0.129 (latest v0.142.3), so its contracts are the ones to treat as moving targets. The shim layer (decision 1) absorbs breaking changes in one place; the mitigation and version-pinning policy are tracked in the risk register.

Correction (2026-06-27). An earlier draft listed a second hard gap: “Gemini has no on-demand skill-injection analog.” Verification against the repo refuted it. Gemini CLI ships a full Agent Skills system (.gemini/skills/SKILL.md, progressive disclosure via the activate_skill tool), so Gemini ports skills natively, exactly like Codex. The fold-into-system-prompt workaround that draft proposed is no longer needed.

Decisions made

  1. Chosen strategy: a hybrid of a shared host-neutral core plus thin per-host binding shims. The canonical core is the portable layer (Markdown bodies, Node hook logic, artifact I/O, envelope convention), maintained once. Per host, a thin shim provides only the four blocking bindings: (a) the manifest/config format, (b) the hook stdin/stdout schema adapter, (c) the plugin-root/project-dir env resolution, and (d) the slash-entry registration. Shims may be generated or hand-written per host; either way they are small and isolated.
    • Why: the expensive, divergent, high-churn surface is exactly the bindings (three different manifest formats, three hook schemas, still-moving host APIs), while the stable, valuable surface, the 64 agent/skill bodies and 4 hook logic files, is already portable. The hybrid boundary lines up with the natural portable/non-portable seam, so it minimizes both duplication and the blast radius of churn.
    • Serves whom: Team’s maintainer and the #56/#57 port-epic implementers. They edit behavior once and re-bind per host, instead of maintaining three drifting copies.
  2. Rejected: single source of truth plus a full transpile/build. One canonical set; a build step emits a complete Claude plugin, Gemini extension, and Codex package. Why rejected: it forces the build to fully model three divergent manifest/agent/command formats, the youngest of which (Codex’s) are still moving. The upfront modeling cost is high and the build itself becomes the highest-churn artifact, since every host API change breaks the transpiler. The hybrid keeps the same DRY core without committing to a total-coverage transpiler; shims can stay hand-written where generation isn’t worth it. The hybrid can generate shims later where it pays, making it a strict superset of this option’s value with less risk.

  3. Rejected: per-host maintained adapters (parallel hand-maintained trees). Why rejected: 3× maintenance across 13 agents, 51 skills, and 4 hooks, plus guaranteed drift, since a fix to an agent body would have to be hand-applied three times. It throws away the fact that the bodies are already portable. Its only advantage (each host fully idiomatic) is largely preserved by the hybrid, since the shim layer is where host idiom lives anyway.

  4. MCP is documented as a bridge, not adopted as the strategy’s mechanism. The matrix records MCP’s reach: tools and resources on both hosts, prompts-as-slash on Gemini only and not Codex. But the chosen path is native per-host bindings, and MCP is a documented fallback to revisit only if a native binding proves insufficient. Why: Codex MCP carries tools and resources but not prompts, so MCP can never be the uniform slash-command layer. Leaning on it would force a split path anyway while adding a server dependency. Keeping it as fallback preserves the option without coupling the strategy to it.

  5. Parity target for #56/#57: full hook and subagent parity, not MVP-first. Each epic targets all four hook events, parallel and nested subagents, and structured returns before declaring done. This raises the bar against the young-API and open-bug risk, so the design confronts those risks head-on (see the risks and “what #56/#57 execute against” below) rather than deferring them by reducing scope.

  6. Per-project configuration lives in a host-neutral .team/config.json. The artifact, the host-agnostic settings it holds (model-tier→host-model map, host selection, per-host parallelism caps, multi-repo list), and its relationship to the per-host manifests are specified under Desired end state.
    • Why: it pulls the one irreducibly host-varying value out of the portable definitions, since the agent model: frontmatter is a Claude-specific model name and meaningless on Gemini or Codex, and puts it behind a single host-agnostic indirection, so the 64 agent/skill bodies never carry a host-specific model literal. The per-host shims read .team/config.json; they never restate it.

What #56 and #57 execute against

Both epics build the hybrid core plus a per-host shim for their host, targeting full parity. Each starts from the matrix and works around the named gaps.

#56. Gemini port

#57. Codex port

Out of scope

Edge cases

These are the boundary conditions the strategy and the downstream epics must handle.

Open questions (deferred to the port epics)

Risks

See also