An adaptive, context-driven Claude Code plugin that coordinates N role-framed OpenAI Codex agents around one shared goal. It is general-purpose with a programmatic center of gravity—project implementation, computer science, software and ML/AI engineering, DevSecOps, debugging/testing, and technical research—while adapting beyond those domains. Agents investigate, build, diagnose, research, challenge, or review from distinct lenses; Claude reconciles their contributions into one coherent outcome.
- Claude Code — authenticated (
claudein terminal) - OpenAI Codex CLI — authenticated (
codexin terminal)
Both must be logged in and working in your terminal before using this plugin.
claude plugins marketplace update
claude plugins marketplace add ehzawad/codex-council
claude plugins install codex-council@codex-councilPersists across sessions — no flags needed.
Iterating on the plugin itself? See For development for the author dev loop (symlink + SessionStart hook).
/codex-council:codex-council
The plugin ships a single skill (no separate commands/ directory), so the
slash form relies on Claude Code exposing plugin skills as slash commands — a
capability of current Claude Code releases whose exact minimum version is not
documented in the changelog. If the slash command is not available on your
version, invoke the skill with the natural-language triggers below instead;
they work on any plugin-capable release.
Claude reconstructs the live task model before composing roles: the problem and project being implemented, what the user has edited or asked, in-flight files, modules, objects, drafts, queries, tests, deployments, and research, active bugs/errors and hypotheses, known unknowns and blind spots, unstated or possibly wrong assumptions, and whether the work is converging or still exploratory. It answers those contextual questions from conversation and workspace evidence, asks the user only when a missing choice materially changes the work, announces the resulting panel, and launches without a manual approval gate.
Strong natural-language triggers are:
ask codex council
codex council review
ask the codex coterie
codex coterie review
ask the codex team
reconcile with the codex team
The slash command or any of those three names means Codex Council. Nearby terms
such as "agent team," "subagents," "council review," or "fan out to agents"
are interpreted from the surrounding conversation rather than rejected by a
missing-string rule. Only genuinely ambiguous requests prompt a choice, with
codex-council (Recommended) first and Claude dynamic workflow (ultracode)—
Claude Code's built-in Agent subagents—second.
There is no built-in role catalog. Claude composes the panel on-the-fly per
invocation from the live problem, evidence, trajectory, uncertainty, and work
ownership—drafting role IDs, labels, and instructions tailored to what the user
is actually doing, then passing them to the script via --roles-file
(a path to the panel JSON) and --context-file (a staged context file
in the same private per-run directory). The script is a pure runner:
it reads those staged inputs, validates them before
launch, fans out bounded-parallel codex exec subprocesses, and aggregates
the replies. It imposes no size ceiling on the panel, role IDs, labels,
instructions, staged context, stdin, or composed prompts, and it never
truncates them. Actual model/provider context windows and available machine
memory remain external constraints and are surfaced as downstream failures.
Collaboration is shared-context and Claude-mediated: parallel roles do not secretly chat with one another, so Claude reconciles each round and can feed material findings into a focused follow-up round. For implementation in one workspace, one executor/integrator owns writes by default while other roles inspect, test, research, or propose; multiple writers require isolated worktrees or serialized phases.
For long Claude Code sessions, "full context" means a decision-complete working set rather than a raw transcript dump: the project/problem and current trajectory; in-flight modules, artifacts, tests, errors, hypotheses, and research; recent work in high fidelity; live primary evidence; known unknowns, blind spots, assumptions, and provenance; plus older still-relevant history summarized with its decisions, rejected paths, and invariants.
General-purpose, with honest capability bounds. The council uses an AGI-style adaptive collaboration pattern: Claude derives roles from the actual work instead of selecting from a domain catalog. Its strongest lean is complex programmatic problem-solving—implementation, software/ML systems, DevSecOps, testing, diagnosis, and evidence-based technical research—but the role synthesis remains situational across other work. This is not a claim that the underlying models are proven AGI; results still depend on the active Codex model, tools, evidence, and task.
The JSON role spec, retries, and panel-proposal flow are documented in
plugins/codex-council/skills/codex-council/SKILL.md.
flowchart LR
User["User"] --> Claude["Claude Code"]
Claude --> Skill["codex-council skill<br/>SKILL.md"]
Skill --> Panel["Compose task-specific role panel<br/>Announce and launch"]
Panel --> Script["codex_council.py<br/>--roles-file + --context-file"]
subgraph Plugin["codex-council plugin"]
Manifest[".claude-plugin/plugin.json"] -.-> Skill
Script --> Validate["Launch-side privacy gate<br/>Validate staged inputs<br/>Parse and validate roles"]
Validate --> Prompt["Bookend context with<br/>each role instruction"]
Prompt --> Fanout["asyncio.Semaphore + gather<br/>bounded parallel fan-out"]
Fanout --> RoleA["Role runner A"]
Fanout --> RoleB["Role runner B"]
Fanout --> RoleN["Role runner N"]
RoleA <--> State["Per-project/session/role state<br/>$XDG_STATE_HOME/codex-council"]
RoleB <--> State
RoleN <--> State
RoleA --> Live["Liveness layer per subprocess<br/>incremental stdout/stderr readers<br/>output-inactivity watchdog<br/>CODEX_COUNCIL_STALL_SECS"]
RoleB --> Live
RoleN --> Live
end
subgraph Codex["Codex CLI subprocesses"]
Live --> ExecA["codex exec resume or fresh"]
Live --> ExecB["codex exec resume or fresh"]
Live --> ExecN["codex exec resume or fresh"]
end
ExecA --> JSONL["JSONL events"]
ExecB --> JSONL
ExecN --> JSONL
JSONL --> Parse["Extract thread.started<br/>Extract final agent_message"]
Parse --> Report["Aggregated markdown report"]
Report --> Claude
Claude --> Reconcile["Claude reconciles results<br/>for the user"]
sequenceDiagram
participant U as User
participant C as Claude Code
participant F as Private run dir
participant S as codex_council.py
participant X as Codex CLI
U->>C: Invoke codex-council
C->>C: Compose task-specific role panel
C->>U: Announce panel
C->>F: mktemp -d once
C->>F: Write roles.json and context.md
C->>S: --check-staging-dir F --skill-contract 1
S-->>C: staging OK or precise staging error
C->>S: --roles-file F/roles.json --context-file F/context.md --skill-contract 1
S->>S: launch privacy gate re-validates each input's parent dir
par role fan-out
S->>X: codex exec role A
S->>X: codex exec role B
S->>X: codex exec role N
end
X-->>S: stdout/stderr bytes reset each role's quiet clock
S->>F: err.log heartbeat with quiet=Ns and watchdog=Ns
loop stall policy when quiet reaches CODEX_COUNCIL_STALL_SECS
S->>X: SIGTERM then SIGKILL the stalled attempt
S->>S: success-with-warning, retriable retry, or terminal stall
end
X-->>S: JSONL events
S->>F: out.md report
S->>F: err.log progress + CODEX_COUNCIL_DONE
C->>F: Read out.md and err.log
C->>U: Reconciled answer
flowchart TD
Project["Git repo root or cwd"] --> ProjectHash["project hash"]
Role["Role id"] --> RoleKey["role key"]
Explicit["CODEX_COUNCIL_SESSION_KEY"] --> Scope{"explicit key set?"}
Auto["Auto-detected host session<br/>Claude session, CODEX_THREAD_ID,<br/>TERM_SESSION_ID, TMUX_PANE, STY, VSCODE_PID"] --> Scope
Disable["CODEX_COUNCIL_DISABLE_AUTO_SESSION_KEY=1"] --> Scope
Scope -->|"explicit"| SessionHash["session hash"]
Scope -->|"auto"| SessionHash
Scope -->|"disabled or unavailable"| ProjectOnly["project-wide scope"]
ProjectHash --> StatePath["state path"]
SessionHash --> StatePath
ProjectOnly --> StatePath
RoleKey --> StatePath
StatePath --> Lock["POSIX lock per state file"]
Lock --> Resume["resume stored Codex thread"]
Lock --> Fresh["or start fresh thread"]
Council state lives at
$XDG_STATE_HOME/codex-council/{project-hash}-{session-hash}__{role-key}.json
when the runner can detect a stable host-session id. It auto-detects common
values such as Claude session ids, CODEX_THREAD_ID, TERM_SESSION_ID,
TMUX_PANE, STY, and VSCODE_PID, so separate terminal tabs/panes in the
same repo do not normally share role threads (except multiple integrated
terminals in the same VS Code window, which share VSCODE_PID; set
CODEX_COUNCIL_SESSION_KEY to isolate those). Follow-up calls from the same
host session still resume the same per-role thread. Long role IDs use a
deterministic hashed filename key, while the full ID is preserved in reports,
prompts, and state metadata; this avoids filesystem filename-length failures
without imposing an ID-length limit.
CODEX_COUNCIL_SESSION_KEY remains an explicit override for custom scoping
per branch or task. Set CODEX_COUNCIL_DISABLE_AUTO_SESSION_KEY=1 only if you
want the older project-wide state file shape:
{project-hash}__{role-key}.json.
Codex runs with --dangerously-bypass-approvals-and-sandbox — no
approval prompts, no filesystem sandbox. This gives every Codex
sub-agent full read/write access to your machine so it can thoroughly
inspect the project. Do not use this plugin on untrusted projects or
with untrusted input — a prompt injection inside reviewed content can
steer all N agents.
The same bypass applies when reviewing any non-code material — a prompt injection inside a Markdown draft, a CSV column header, or a research excerpt is just as effective as one inside a code diff, and non-code content has historically been less hardened against injection than code review flows. Be deliberate about what you pipe in.
The script uses your Codex CLI defaults — model, reasoning effort, and
other settings come from ~/.codex/config.toml. No model is hardcoded.
Sandbox and approval settings are overridden by the plugin (see
Security above).
Active role concurrency defaults to 6, matching the current
Codex configuration default
for agents.max_threads. If a positive user-level agents.max_threads is present,
the runner uses it as a conservative local concurrency signal; set
CODEX_COUNCIL_MAX_PARALLEL to a positive integer for an explicit council-only
override. Panels may be larger than active concurrency: excess roles queue in
the runner rather than being rejected or launched simultaneously. Because this
plugin launches separate codex exec processes, Codex's in-process agent
setting is a useful local preference, not a provider-capacity guarantee.
The council has no total elapsed-time or run-level deadline. A role may run
indefinitely while its codex subprocess continues producing output bytes, and
a council call takes as long as its slowest role. Separately, each codex
subprocess has an output-inactivity watchdog based only on the time since
its most recent stdout/stderr byte: after CODEX_COUNCIL_STALL_SECS seconds
of council-visible silence (default 1800; positive integer override; 0
disables), the runner terminates that attempt and applies the stall policy —
retried as [retriable:stall] when no tool work had begun,
success-with-warning when the turn had already completed, terminal [stall]
otherwise. Setting 0 may again permit an indefinitely silent role. Byte
silence is not proof of a wedge: current codex exec --json suppresses
agent-message/reasoning deltas, so a healthy role can be byte-silent for long
stretches — the heartbeat's quiet=Ns measures bytes, not progress. Codex's
own per-provider stream-idle guard
(model_providers.<id>.stream_idle_timeout_ms) remains a separate,
provider-scoped control in your own ~/.codex/config.toml. Ctrl+C tears down
every codex process group. While work remains, the runner writes a status
heartbeat to the staged err.log — cadence adapts to the watchdog
(stall_secs / 3, bounded 300–1800s; every 600s at the default watchdog,
every 1800s when disabled) and each line carries per-role quiet=Ns (or
retry-wait during backoff), the watchdog= threshold, and the plugin
version=. The skill uses Claude Code's native
background-task mechanism
and, where available, session-cron state
to surface progress without a shell polling loop.
v0.9.0 behavior change — launch-side privacy gate. The launch path now
re-validates what the preflight validates: every on-disk input's parent
directory must be a private (0700), user-owned, non-symlink directory at
launch as well as at --check-staging-dir time, checked before any content is
read. Migration note for direct CLI users: stage roles.json (and
context.md when used) in a private directory, e.g. one created by
mktemp -d; a public or symlinked parent is refused with an
abandon-this-directory recovery.
git clone https://github.com/ehzawad/codex-council.git
cd codex-council
claude plugins marketplace add ehzawad/codex-council # skip if already added
claude plugins install codex-council@codex-council # skip if already installed
./scripts/dev-link.sh
# restart Claude Code oncescripts/dev-link.sh does three things:
- Creates a symlink at
~/.claude/plugins/cache/codex-council/codex-council/<version>/→ this repo's working tree, so edits are live at runtime. - Rewrites
~/.claude/plugins/installed_plugins.jsonso the harness'sinstallPathandversionfields point at the symlinked version. - Prunes any stale sibling entries in the cache dir for other versions, so bumping
plugin.jsonand re-running dev-link doesn't leave old directories or symlinks behind.
Step 2 is load-bearing: the harness loads whichever installPath the manifest declares, not whichever symlinks exist in the cache. Without the manifest rewrite, bumping the version in plugin.json and re-running dev-link creates a new symlink that the harness will happily ignore.
Two skew guards help here. The runner prints version=<plugin version> in the
preflight "staging OK" line, the dispatch line, the heartbeat, and the
CODEX_COUNCIL_DONE sentinel — postmortem visibility into which plugin
version actually ran, not skew prevention. And SKILL.md's command templates
pass --skill-contract 1: if the linked script's contract epoch differs, the
invocation is refused (exit 2) as a stale SKILL/script pair — re-run
scripts/dev-link.sh (or reinstall the plugin) and restart the session.
After the one-time restart, edits to plugins/codex-council/** are live on the next /codex-council:codex-council invocation. SKILL.md caveat: the Claude Code harness's skill-content caching behavior is not documented, so SKILL.md edits may still require a session restart; the script and the rest of the plugin files update live.
Startup-overwrites-symlink caveat. Claude Code re-validates the plugin cache on every session start and replaces the symlink with a freshly-fetched copy from origin. The documented "symlinks are preserved" property applies to runtime resolution, not startup validation. Two ways to handle it:
- Manual: re-run
./scripts/dev-link.shafter every Claude Code restart, anyclaude plugins update, any version bump inplugin.json(the cache path changes with the version), or any cache wipe. - Automatic (recommended): add a
SessionStarthook to~/.claude/settings.jsonso the symlink is re-established on every session:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash -lc 'mkdir -p \"$HOME/.claude/logs\"; log=\"$HOME/.claude/logs/codex-council-dev-link.log\"; \"/absolute/path/to/codex-council/scripts/dev-link.sh\" >>\"$log\" 2>&1; rc=$?; if [ \"$rc\" -ne 0 ]; then printf \"%s dev-link failed exit=%s\\n\" \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$rc\" >>\"$log\"; fi; exit 0'"
}
]
}
]
}
}Failures remain fail-open (exit 0) so a missing repo or broken dev-link script
never blocks session startup, but diagnostics are logged to
~/.claude/logs/codex-council-dev-link.log. Keep this fail-open behavior limited
to the development startup hook; council launch/context pipelines in SKILL.md
should fail closed with set -euo pipefail. Merge into your existing
hooks.SessionStart array if you already have one (don't replace it).
MIT