claude-coterie
An OpenAI Codex CLI skill that orchestrates a coterie of persistent Claude Code agents. Codex composes a task-specific panel, fans the shared context out in bounded parallel — one persistent Claude Code agent per role — and reconciles the aggregated markdown report into one outcome. Each role keeps its own resumable Claude thread per project.
Codex is the host and orchestrator; Claude Code supplies the council roles.
This is the mirror-inverse of
ehzawad/codex-council with the
hosts swapped. A coterie can contain one role when the work needs one focused
lens; that is a one-role panel using the same council workflow.
What this guarantees
The council fans out at the orchestrator layer: one subprocess per role, each
with its own role:<id> session key, bounded by
CLAUDE_COTERIE_MAX_PARALLEL (default 6). A role call adds no wrapper-level
wall-clock timeout, turn limit, budget limit, or stdin/stdout size cap — it
blocks until that role's Claude exits or the run is cancelled. A role runs at
most one claude -p process at a time per session key (one launch, plus one
sequential fresh retry after a confirmed stale resume). Role calls do not pass
--max-turns, --max-budget-usd, or --no-session-persistence.
Those guarantees apply to this tool. The operating system, invoking shell/tool, Claude Code CLI, account, network, and model still have their own intrinsic limits. In particular, an outer Codex command timeout can still terminate a subprocess; the skill instructs Codex not to impose one.
Prerequisites
- OpenAI Codex CLI, authenticated
- Claude Code, authenticated (
claude auth status) - POSIX-compatible file locking (
fcntl), so Linux and macOS are supported by this implementation
Install
Codex discovers user-scoped skills under $CODEX_HOME/skills (default
~/.codex/skills). Install with Codex's built-in installer from inside Codex:
$skill-installer install the claude-coterie skill from https://github.com/ehzawad/claude-coterie
or run the installer script directly:
python3 "${CODEX_HOME:-$HOME/.codex}/skills/.system/skill-installer/scripts/install-skill-from-github.py" \
--repo ehzawad/claude-coterie --path . --name claude-coterie
Manual clone (equivalent):
git clone https://github.com/ehzawad/claude-coterie.git \
"${CODEX_HOME:-$HOME/.codex}/skills/claude-coterie"
For development from another checkout:
ln -s "$(pwd)" "${CODEX_HOME:-$HOME/.codex}/skills/claude-coterie"
Restart Codex to pick up the new skill.
Update
For a cloned install:
python3 "${CODEX_HOME:-$HOME/.codex}/skills/claude-coterie/scripts/update_skill.py"
The updater refuses to overwrite local edits and uses a fast-forward-only pull.
(For a $skill-installer install, re-run the installer instead.)
Usage
Invoke the skill deterministically:
$claude-coterie
Natural requests such as “claude council”, “claude coterie”, and “claude team” also activate it. Codex composes the panel from the actual work (see SKILL.md), then launches:
RUN=$(mktemp -d "${TMPDIR:-/tmp}/claude-coterie.XXXXXX")
# write $RUN/roles.json (panel) and $RUN/context.md (shared brief), then:
python3 "$CODEX_HOME/skills/claude-coterie/scripts/claude_coterie.py" \
--check-staging-dir "$RUN" --skill-contract 1
python3 "$CODEX_HOME/skills/claude-coterie/scripts/claude_coterie.py" \
--roles-file "$RUN/roles.json" --context-file "$RUN/context.md" \
--skill-contract 1 > "$RUN/out.md" 2> "$RUN/err.log"
roles.json is a list of {id, label, instruction} objects; instruction is a JSON array of sentence-sized strings whose joined text must include "nothing material" and end with "Thoroughness beats speed." (the codex-council role contract, mirrored). The report on stdout aggregates every role's reply; stderr carries per-role progress lines plus the final ~2,000 characters of each role transport's diagnostics, and — on normal completion, once the report is flushed — ends with the CLAUDE_COTERIE_DONE sentinel. Exit codes: 0 when at least one role responded, 1 when all failed, 2 for usage errors, 128 + signal when cancelled.
Roles are analysis-only by default; pass --allow-edit to the orchestrator when roles should mutate files, and then keep a single writer.
Per-role context resume
A fresh role call lets Claude allocate its session ID. The council runtime stores that ID under:
$XDG_STATE_HOME/claude-coterie/{project-hash}-{session-hash}.json
The default state root is ~/.local/state/claude-coterie/. The {project-hash} is derived from the canonical Git worktree root (outside Git, the canonical current directory); the {session-hash} is derived from CLAUDE_COTERIE_SESSION_KEY, which the orchestrator sets to role:<id>. So each role has its own state file, and calls made from different subdirectories of the same checkout resolve to the same state.
The Claude subprocess runs with the canonical project root as cwd. That detail is essential: Claude Code associates resumable sessions with project directories/worktrees. Follow-up councils use --resume <session-id> per role, so each role's transcript and accumulated context continue across separate councils in the same project.
Ordering and concurrency
Each Claude thread is single-writer. A per-project/per-session .run.lock is held across:
load session -> resume or start fresh -> receive final result -> save session
That serialization prevents two invocations from writing interleaved messages into the same Claude session. Different projects or explicit session keys proceed independently — which is exactly how the council parallelizes: every role runs under its own role:<id> session key, so distinct roles hold distinct run locks and fan out concurrently while repeated calls to the same role stay ordered. A nested same-key invocation from inside a running Claude call fails fast instead of deadlocking.
State writes use atomic replace, corruption quarantine (including structurally invalid state), generation-aware compare-and-save, compare-and-clear, and stale-session fallback. If a stored session no longer exists, the runtime clears only the matching stale generation and starts fresh.
Input and output behavior
Each role receives the shared context on stdin in full; its role instruction
rides on --append-system-prompt so stdin stays pure context. Claude's
result string is returned in full and placed in the aggregated report. The
runtime does not truncate either side.
This does not make the model context window infinite. For very large repositories, context selection still matters: send the narrowest complete evidence set that supports the review, and let Claude inspect the project directly when appropriate.
Execution and cancellation
claude --help capability detection and the main claude -p call both run without wrapper timeouts.
Pressing Ctrl-C — or an outer supervisor sending SIGTERM/SIGHUP — is treated as explicit cancellation. Cancelling the orchestrator forwards SIGTERM to every live role subprocess, each of which terminates its Claude process group and reaps it with a bounded drain, so child tool processes do not remain orphaned and cancellation always returns promptly.
Security and authentication routing
Claude runs with --dangerously-skip-permissions. Roles are analysis-only by default: a safety directive ("Do not modify files or run mutating commands; provide analysis only") is appended to every role instruction unless --allow-edit is passed. Do not run the coterie on untrusted repositories or prompts.
Each role's child environment strips ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, and ANTHROPIC_BASE_URL by default so an authenticated Claude.ai session is not silently displaced by API-key or proxy routing. Set CLAUDE_COTERIE_KEEP_ANTHROPIC_ENV=1 to preserve those variables intentionally.
Role subprocesses do not use --bare, so normal project instructions and
Claude Code configuration remain available.
Implementation note
scripts/claude_coterie.py is the council entry point Codex drives. See
DESIGN.md for the internal transport, state, and launch flow.
Test
python3 -m unittest discover -s tests -p 'test_*.py' -v
License
MIT
No comments yet
Be the first to share your take.