Add OpenAI Codex as a first-class ctx agent - #163
Open
parlakisik wants to merge 17 commits into
Open
Conversation
…ants OpenAI Codex ships hooks and plugins as stable features with a lifecycle-hook contract mirroring Claude Code's. This lays the delivery foundation: internal/assets/codex is a Codex plugin root (.codex-plugin/plugin.json, hooks/hooks.json, .mcp.json, 50 skills generated from the canonical Claude skills by hack/sync-codex-skills.sh with allowed-tools stripped, Claude-only skills excluded, and each skill's references/ mirrored), referenced by a repo marketplace at .agents/plugins/marketplace.json (install: codex plugin marketplace add ActiveMemory/ctx; codex plugin add ctx@activememory-ctx). The hooks manifest maps the Claude wiring onto Codex events — SessionStart carries the ctx agent packet (plain stdout becomes developer context), update_plan replaces EnterPlanMode, apply_patch aliases Edit|Write, SessionEnd runs the journal import inside Codex's 3-second cap — every command anchored to the git root because ctx is CWD-anchored and Codex runs hooks from the session cwd. internal/config/codex centralizes the constants (home layout, plugin identity, TOML tokens, hook events, rollout vocabulary, injected-user prefixes). Version plumbing (make sync-version / check-version-sync, hack/build-all.sh, hack/release.sh) now covers the Codex manifest and marketplace; make sync-codex-skills / check-codex-skills guard skill freshness and run from make build / make audit. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
ctx setup codex --write deploys the project-local route: merges .codex/hooks.json (foreign matcher groups preserved byte-for-byte in meaning, stale ctx-managed groups replaced; ownership requires the git-root anchor plus a ctx invocation so user hooks copying the anchor idiom are never touched), appends [mcp_servers.ctx] to .codex/config.toml without ever parsing the user's TOML (line scans with no 64KB scanner ceiling; existing bytes never rewritten), deploys AGENTS.md via the shared agents deployer, and writes .agents/skills/<name>/SKILL.md plus references/ (create / refresh-if-stale / reject-foreign). Everything is idempotent. internal/codex holds the pure helpers: Home ($CODEX_HOME), plugin detection against the install cache and config.toml (Absent → PluginNotInstalled → PluginInstalledNotEnabled → PluginReady), MergeHooks, EnsureMCPTable, SkillManaged. When the ctx plugin is enabled, --write short-circuits to AGENTS.md with a plugin-mode summary so hooks never load twice. ctx setup codex without --write prints the overview, both install routes, the /hooks trust step, and the detection state; ctx init hints at the setup when codex is on PATH and unwired. Live-verified against Codex 0.148: SessionStart packet injection, all 13 UserPromptSubmit nudges, PreToolUse blocking via the legacy decision shape on the unified-exec path, and SessionEnd journal import within the cap. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
ctx journal import now discovers $CODEX_HOME/sessions (default ~/.codex/sessions), where Codex writes one rollout-*.jsonl per thread under YYYY/MM/DD. The parser assembles one session per file from session_meta (id, cwd, git branch, originator), response_item messages (developer-role and Codex-injected user items — environment context, instruction payloads, "# AGENTS.md instructions for <path>" — are filtered so titles and first-user-message come from real prose), function_call / custom_tool_call / local_shell_call tool traffic, turn_context (model), and the last cumulative token_count. Sessions with no user prose are skipped; malformed lines are tolerated; the scanner uses the 4MB schema-checker ceiling because Codex tool-output lines can exceed 1MB. Fixtures are sanitized from a real Codex 0.148 rollout plus hand-written edge cases. The Claude-JSONL schema drift checker is now gated to claude-code sessions so Codex imports stop producing "unknown record types" noise. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
ctx steering sync with tool: codex (or claude) in .ctxrc — or via
--tool — errored with 'unsupported sync tool' even though the docs
promise these tools are simply not sync targets: they consume
steering through ctx agent and AGENTS.md. The sync command now asks
steering.ConsumesDirectly first and prints an info line ("<tool>
consumes steering via ctx agent; nothing to sync") with exit 0.
Unknown tools still error exactly as before, and SyncTool's own
contract is unchanged.
Closes the live bug recorded in the 2026-08-19 session journal.
Spec: specs/codex-integration.md
Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Guards that keep the Codex delivery honest: the hooks manifest only uses supported events, every handler anchors to the git root, the UserPromptSubmit list matches Claude's in order, and the PreToolUse/PostToolUse sets stay in explicit correspondence with the Claude manifest (EnterPlanMode -> update_plan, ctx agent -> SessionStart) so a hook added to one manifest fails until mirrored. Plugin manifest, .mcp.json, and the repo marketplace are checked against VERSION and the mcp server constants; the Codex skill set must equal the Claude set minus the four Claude-only skills, with allowed-tools stripped; every references/ path a shipped skill cites must exist in the embed. The hooks-wiring and check-audit isolation guards now iterate both shipped manifests, and the skill frontmatter walk covers the Codex tree. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
docs/home/codex.md walks both install routes (plugin marketplace and ctx setup codex --write), the /hooks trust step, the exact event mapping, journal import, and the honest limitations (3s SessionEnd cap, trusted-project requirement — including that a -c trust override does not unlock project layers — no Windows overrides, memories out of scope). docs/operations/integrations.md gains the full Codex section with executable drift-check comments mirroring the Claude ones; setup/journal/steering/system CLI pages, the multi-tool recipe, getting-started, configuration, and the README now cover codex; the stale block-dangerous-command reference in docs/cli/system.md is corrected. Nav updated so the docs reachability guard stays green; EXTENSION-POINTS lists the new deployer package. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
make audit died on macOS before checking anything: /bin/bash is bash 3.2.57, whose command-substitution re-parser treats an apostrophe in a comment inside $( ... ) as an open quote, so the script aborted with 'unexpected EOF' (exit 2). With that fixed, the struct-field counters' grep -cP failed silently on BSD grep (no PCRE), emitting 59 MISSING_FIELDS false positives. Reworded the comment and switched both counters to grep -cE with a literal tab and [[:space:]]. The script now runs clean on macOS, matching Linux CI. Spec: specs/lint-docstrings-macos-portability.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
TASKS gains the Codex phase (CX1-CX6 complete; CX7 Windows commandWindows follow-up open). DECISIONS records the plugin-root placement with the repo marketplace, the never-parse-TOML append-and-scan strategy, and Codex memories as a non-goal. LEARNINGS captures the golangci-lint cache-corruption signature, the macOS shell/grep portability traps, and the Codex 0.148 trust and hook facts verified live. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
ctx setup codex said "ctx plugin not installed" even in a project where the project-local route was already deployed, which read as not-configured. The status line now prefers the deployment truth: when .codex/hooks.json exists it prints "project-local integration configured"; the plugin detection states remain the fallback. The overview also stops framing Codex relative to Claude Code and names what the hooks actually do (context packet on session start, persistence nudges, command gates, journal capture). Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
…ting Installing the plugin from a source revision that predates the Codex marketplace silently falls back to the legacy .claude-plugin/marketplace.json and delivers the Claude plugin variant, whose CLAUDE_PROJECT_DIR-anchored hooks cannot run under Codex (hit live installing from GitHub main; when both marketplace files exist Codex 0.148 prefers the .agents one). Troubleshooting now covers detecting the wrong variant (.claude-plugin/ in the cache root), the remove-and-re-add fix, and cleaning project-local hooks when switching to the plugin route so hooks never run twice. Learning recorded. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
A customer without Claude Code can end up with a ctx plugin whose cached copy is the legacy Claude variant (installed from a marketplace source that predates .agents/plugins/marketplace.json) or with a stale enabled flag and no cache. Its hooks cannot run under Codex, yet ctx setup codex --write treated "enabled in config.toml" as "working" and skipped the project-local deploy. The plugin-enabled short-circuit now also requires the installed cache to carry .codex-plugin/ (PluginCodexVariant). When the flag is set but the variant check fails, the deployer warns with the exact remove/re-add fix and deploys the full project-local route, so Codex users always end up with working hooks. The init hint (Unwired) and the ctx setup codex state line apply the same test. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
… works Codex's marketplace resolution is code-path dependent: the CLI resolves the native .agents/plugins/marketplace.json while the 0.149 TUI plugin browser re-materialized the same plugin from the legacy .claude-plugin/marketplace.json — silently swapping the installed cache to the Claude variant, whose CLAUDE_PROJECT_DIR hooks all exit 1 under Codex (observed live). The Claude plugin root now also carries .codex-plugin/plugin.json pointing hooks at hooks/codex.json, a byte-copy of the canonical Codex manifest kept in lockstep by hack/sync-codex-skills.sh and guarded by TestClaudeRootDualManifest. Codex prefers a .codex-plugin manifest when a root has both (verified with a legacy-only scratch marketplace and a marker hook), so whichever marketplace file any Codex code path picks, the installed plugin runs the Codex hooks. Version sync (make sync-version / check-version-sync, build-all, release) covers the new manifest; troubleshooting docs updated. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Codex can run hooks with a cwd outside any git repository (seen live: PreToolUse firing for plugin skill reads under ~/.codex/plugins/cache). There `git rev-parse --show-toplevel` emits nothing and the bare `cd ""` is shell-dependent — under Codex's hook shell it exits 1 before ctx starts, surfacing as "PreToolUse hook (failed): exit code 1" noise on otherwise healthy sessions. The anchor is now `cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"`: inside a repo, identical behavior; outside, ctx runs in place and its no-context graceful paths exit 0 silently. All 21 manifest commands regenerated (Claude-root codex.json in lockstep via the sync script). Merge ownership recognizes the legacy anchor so previously deployed project files migrate to the tolerant form instead of duplicating; regression test covers the migration. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Adversarial audit (3 finders + per-finding verifiers, all repro'd)
after the Codex non-repo-cwd anchor bug confirmed 20 defects in the
same failure classes across the other hook surfaces. Fixes:
Claude manifest: all 22 commands get a uniform POSIX prologue —
exit 0 silently when ctx is absent (ctx is documented optional; a
missing binary must not spam 13 hook failures per prompt), then a
[ -d ] guard with a remedy on stderr and deterministic exit 1 for
unset/stale CLAUDE_PROJECT_DIR. The previous ${VAR:?} abort has a
shell-dependent exit status (127 bash, 1 zsh, 2 dash) and exit 2
means BLOCK to Claude Code, so a dash host with the var unset would
hard-block every tool call. Verified identical behavior under sh,
bash 3.2, zsh, and dash. Parity-test anchor and the
cwd-anchored-context spec's hook-contract row updated.
Copilot CLI: every manifest entry gains the schema-native
"cwd": "." (resolved against the repository root), closing the
launch-from-subdirectory anchor gap portably. Both shipped wrapper
script generations (16 files) were dead code the manifest never
referenced, deployed as clutter into user repos, and carried eight
of the confirmed defects (set -e jq aborts, unbounded $(cat),
string-built JSONL rot, cwd-relative audit paths, a deny payload
using Claude field names on stderr that Copilot ignores, suppressed
PowerShell errors, BOM-less mojibake) — removed at the root along
with the deployer copy loop, embed globs, reader, and constants.
Trace hook: git invokes prepare-commit-msg for --amend with
COMMIT_SOURCE=commit plus a SHA, which the merge|squash filter
missed, appending a duplicate trailer per amend; amends now exit
early and a same-key trailer guard makes the append idempotent.
plugin-reload.sh: stages into mktemp and swaps atomically so a
mid-build failure can't destroy the existing cache; mirrors the
whole plugin root, closing the known .mcp.json dev-reload gap.
OpenCode: stdin-reading ctx calls run with < /dev/null (a
host-held pipe cost the 2s stdin timeout per call); header
documents the git-worktree requirement; MCP registration falls
back to os.Executable() when ctx is not on PATH at setup time
instead of writing a bare name OpenCode cannot spawn.
Deliberate non-fixes, documented in place: kiro/cursor/cline MCP
configs keep the bare ctx command because those files are
project-scoped and committable — embedding a machine-specific
absolute path would break the rest of the team.
Verified per tool with fresh deploys of all nine integrations in a
scratch project (agents, kiro, cursor, cline, copilot, copilot-cli,
opencode, codex, claude-code) plus steering sync: all green.
Spec: specs/hook-surface-robustness.md
Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
LEARNINGS captures the seven hook failure classes and the shell/cwd/stdin test matrix any new hook surface must pass; TASKS adds CX8 (Windows parity: Codex commandWindows overrides and a cross-shell ctx-absent guard for the Copilot manifest). Spec: specs/hook-surface-robustness.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Correctness: - Codex hook commands gain the ctx-absent guard (exit 0 silently when ctx is not on PATH) via a HookPrologue shared with the guards and merge ownership; legacy guardless deployments still migrate cleanly. - Journal import moves to an async Stop hook with a 120s budget (Codex caps SessionEnd at 3s, which silently killed imports on large histories); SessionEnd stays as the final incremental sweep, and the parity guard encodes Stop as Codex-only. - TOML header detection (MCP table and plugin-enabled) tolerates equivalent spellings — trailing comments, whitespace, quoted key segments — so setup never appends a duplicate [mcp_servers.ctx] table that would invalidate the user's config.toml. - Mixed hook groups: a user handler added inside a deployed ctx group now survives merges (ctx handlers are stripped from the group, the fresh embedded groups replace them); pure-ctx groups are replaced as before. Regression tests for both behaviors. - The plugin-enabled short-circuit warns when a project-local .codex/hooks.json coexists with the plugin (hooks would run twice) and names the file to remove. - A failed hooks merge no longer prints the success summary and /hooks trust reminder; deployHooks reports deployment state and Deploy gates the summary on it. - make check-codex-skills now also diffs (and restores on failure) the dual-manifest files instead of silently rewriting them. Journal parser parity: - Sessions with failing commands set HasErrors (from item_completed CommandExecution exit codes). - TotalTokensIn subtracts cached prompt tokens, matching the Claude parser's accounting. - TurnCount counts every user-role message (Claude semantics); the no-prose skip rule is preserved separately. - Preview truncation cuts on rune boundaries (Claude parser's identical flaw fixed via the shared helper). - CodexSessionDirs reuses codex.Home() instead of re-implementing CODEX_HOME resolution. Simplification: - The claude-root Codex plugin manifest is now generated by hack/sync-codex-skills.sh from the canonical manifest (hooks path rewritten), ending the hand-maintained divergence. One reported finding was refuted, not fixed: additionalContextLimit is a token threshold per Codex's docs and the live test delivered the full packet. Spec: specs/codex-integration.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Codex installs of the ctx plugin were emitting 13 hook failures per prompt (plus more per tool call) in every session, even in projects where `ctx setup codex` was never run. Cause: Codex can materialize its plugin cache from the legacy `.claude-plugin` marketplace entry, which delivers the Claude plugin root, and it then loads that root's `hooks/hooks.json` — whose commands require `CLAUDE_PROJECT_DIR`, a variable only Claude Code sets. Every command therefore aborted before ctx ran. The manifest now treats an unset `CLAUDE_PROJECT_DIR` as "not running under Claude Code, not my hook" and exits 0 silently, while keeping the loud failure (exit 1 + remedy) for the diagnosable case: the variable is set but its directory is gone. Behavior under Claude Code is unchanged. Verified in all four states across sh, bash, zsh, and dash. This closes the noise regardless of which manifest a given Codex version resolves, including from plugin caches installed before the dual-manifest defense landed. Spec: specs/hook-surface-robustness.md Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Deploying ctx with
|
| Latest commit: |
9153260
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7278662d.ctx-bhl.pages.dev |
| Branch Preview URL: | https://feat-codex-integration.ctx-bhl.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ctxalready treatedcodexas a tool identifier —.ctxrcschema,drift validation, docs promising hook + MCP parity — but nothing backed
it:
ctx setup codexerrored,ctx steering syncwithtool: codexerrored, and Codex sessions never reached the journal. Codex CLI 0.148
ships hooks and plugins as stable with a lifecycle contract that mirrors
Claude Code's, so the
ctx systemhook runtime is reused unchanged andthis branch builds the delivery layer around it: a Codex plugin root with
a repo marketplace, a project-local deployer, a rollout-transcript parser,
guards, and docs. Using it live surfaced further defects, and an
adversarial audit of every hook surface (Claude, Codex, Copilot CLI,
OpenCode, trace, dev reload) found 20 more in the same failure classes —
all fixed here.
Changes
internal/assets/codex/,.agents/plugins/marketplace.json): manifest,hooks/hooks.json,.mcp.json, and 50 skills generated from the canonical Claude skillsby
hack/sync-codex-skills.sh(stripsallowed-tools:, excludes fourClaude-only skills, mirrors
references/). Version sync andmake check-codex-skillsguard freshness — servesspecs/codex-integration.mdcode-path dependent (the CLI reads
.agents/…, the 0.149 TUI read thelegacy
.claude-plugin/…and silently installed the Claude variant,whose
CLAUDE_PROJECT_DIRhooks cannot run under Codex). The Clauderoot now also carries
.codex-plugin/plugin.jsonpointing atCodex-format hooks, so either path yields a working install — serves
specs/codex-integration.mdctx setup codex [--write](internal/codex/,internal/cli/setup/core/codex/): merges.codex/hooks.json(foreign groups and user handlers preserved), appends
[mcp_servers.ctx]to.codex/config.tomlwithout ever parsing theuser's TOML, deploys
AGENTS.mdand.agents/skills/, detects pluginstate, and refuses to short-circuit on a non-Codex plugin variant —
serves
specs/codex-integration.mdinternal/journal/parser/codex*.go): reads$CODEX_HOME/sessions/**/rollout-*.jsonl, filters Codex-injecteditems, and matches the Claude parser's semantics for turns, tokens
(cache-adjusted), and error flags; the Claude-JSONL schema checker is
gated to Claude sessions — serves
specs/codex-integration.mdctx steering syncwithtool: claudeortool: codexnow prints an info line and exits 0 instead of erroring,matching what the docs always promised — serves
specs/codex-integration.mdhook commands (silent exit 0 when ctx is absent; deterministic exit 1
with a remedy for unset/stale
CLAUDE_PROJECT_DIR— the previous${VAR:?}abort exits 2 under dash, which Claude Code reads as a hardblock);
"cwd": "."on every Copilot CLI hook entry; removal of 16dead Copilot wrapper scripts that carried eight defects; idempotent
commit-trace trailer; atomic
plugin-reload.sh;< /dev/nullonstdin-reading OpenCode calls — serves
specs/hook-surface-robustness.mdhooks-wiring and ctxctl-isolation guards extended to the Codex
manifest,
docs/home/codex.mdplus nine updated pages with executabledrift-check comments — serves
specs/codex-integration.mdmake auditon macOS:hack/lint-docstrings.shaborted underbash 3.2 (apostrophe in a comment inside
$( )) and emitted 59 falsepositives from
grep -Pon BSD grep — servesspecs/lint-docstrings-macos-portability.mdSpecs
specs/codex-integration.md— the Codex delivery layer: plugin andproject routes, event mapping, edge cases, non-goals
specs/hook-surface-robustness.md— the seven hook failure classesfound across every surface and the fixes applied
specs/lint-docstrings-macos-portability.md— whymake auditneverran on macOS and the two portability fixes
Closed tasks
embed directives, plugin root, marketplace, skills sync
internal/codexhelpers +ctx setup codex, dispatch,text, init hint
$CODEX_HOME/sessionsrolloutsLeft open: [CX7]/[CX8] Windows parity (
commandWindowsoverrides and across-shell ctx-absent guard for the Copilot manifest).
Verification
make lint0 issues;go test ./...green across 181 packages;make auditpasses end to end (it could not run on macOS before thisbranch)
injected (~7.9k tokens), all 13
UserPromptSubmitnudges leaving state,PreToolUseblocking a non-PATH./ctxinvocation, and session-endjournal import producing entries — via both the plugin route and the
project-local route
kiro, cursor, cline, copilot, copilot-cli, opencode, codex,
claude-code) plus
steering syncin a scratch project; every fileClaude Code reads is byte-identical to
mainfixed in
012276a7(one refuted with reasoning recorded)