Skip to content

feat(codex): native Codex hooks, thread-bound handoffs, additive installs alongside Claude Code (2.119.0) - #159

Merged
drknowhow merged 5 commits into
mainfrom
feat/codex-native
Sep 5, 2026
Merged

drknowhow merged 5 commits into
mainfrom
feat/codex-native

Conversation

@drknowhow

Copy link
Copy Markdown
Owner

Summary

Native Codex integration. Codex and Claude Code coexist in one repository; installing one host never touches the other's configuration, and every installed launcher passes an explicit --host.

  • Hooks — c3 install-mcp --ide codex installs the C3 dispatcher into .codex/hooks.json for PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart, PreCompact and SessionEnd. Unrelated handlers are kept and hook trust is never bypassed.
  • TOML merge — [mcp_servers.c3] is merged with tomlkit; user keys, comments, subtables and enabled = false survive, and malformed TOML is an error rather than an overwrite.
  • Host identity — core/host.py resolves the provider from --host, then C3_HOST, then the host session env, then the project preference. Delegated children drop inherited CODEX_THREAD_ID / CLAUDE_CODE_SESSION_ID.
  • apply_patch audit — every add, update, delete and move target goes through the existing pre-tool guard and edit ledger; malformed patches deny and failed patches are not logged.
  • Thread-bound handoffs under .c3/host_sessions/codex/, restored only for the same thread and project on SessionStart.
  • c3 doctor --ide codex — CLI support, installed config and observed hook activity, with no model call and no env values printed.
  • Transcripts and usage — bounded rollout import from CODEX_HOME/sessions, latest cumulative token_count, cached input separated.
  • Delegation — codex exec --json with the prompt on stdin, explicit sandbox, approval_policy="never"; resume is bound to the persisted thread id, never resume --last.
  • --global-fallback is now opt-in. The Antigravity fallback write and .mcp.json deletion are gone from the Codex install path.

Docs in docs/codex-native.md, README pointer, CHANGELOG 2.119.0. New dependency tomlkit>=0.13.2.

Implementation by Cod (Codex); review, version bump and CHANGELOG by Yep.

Test plan

  • ruff check . clean
  • pytest — 3707 passed, 6 skipped locally (Windows, Py 3.14). tests/test_ci_act.py::TestActIntegration deselected because Docker registry auth fails before the job runs, same as the existing CI lint job.
  • tests/test_codex_mcp_transport.py — fresh stdio MCP initialize, c3_read and c3_session for both --host codex and --host claude-code
  • CI green on this PR
  • After release — overlay 2.119.0 onto the live install, c3 install-mcp --ide codex on this checkout, trust hooks in Codex, c3 doctor --ide codex reports the hooks installed

https://claude.ai/code/session_0165dDtca3R3ggdkX4rAPJoP

…alls alongside Claude Code (2.119.0)

Codex and Claude Code now coexist in one repository. Installing one host no
longer strips the other's config, every launcher passes an explicit --host,
and Codex gets the same hook chain (PreToolUse, PostToolUse, UserPromptSubmit,
Stop, SessionStart, PreCompact, SessionEnd) via .codex/hooks.json.

- core/host.py resolves the process identity from --host, C3_HOST, the host
  session env, then the project preference; delegated children drop
  inherited thread ids.
- core/mcp_toml.merge_toml_section merges [mcp_servers.c3] with tomlkit and
  keeps user keys, comments, subtables and an explicit enabled=false.
- core/codex_patch + hook_dispatch audit apply_patch per target (add, update,
  delete, move) through the existing guard and edit ledger; malformed patches
  deny, failed patches are not logged.
- cli/hook_codex_lifecycle keeps a thread-bound handoff under
  .c3/host_sessions/codex.
- services/codex_transcript + conversation_store import bounded rollouts,
  take the latest cumulative token_count, and separate cached input.
- delegate runs codex exec --json with the prompt on stdin, an explicit
  sandbox and approval_policy="never"; resume is bound to the persisted
  thread id.
- c3 doctor --ide codex; --global-fallback is opt-in; .codex/hooks.json and
  AGENTS.override.md join the agent-config confirm tier.

Implementation by Cod (Codex). Review, version bump and CHANGELOG by Yep.
Local gates were ruff clean and 3707 passed / 6 skipped (the Docker act
integration test was deselected because registry auth fails before the job
runs, same as on CI).

Claude-Session: https://claude.ai/code/session_0165dDtca3R3ggdkX4rAPJoP
…llama

CI failed on every matrix cell with "unsupported operand type(s) for +:
'float' and 'datetime.timedelta'": CI resolves mcp 2.1.1, where
ClientSession.read_timeout_seconds is a float, while the dev box has
mcp 1.28.1, where it is a timedelta. The test now reads the annotation and
passes whichever type the installed client expects. Dropping the kwarg is
not an option: on mcp 1.x the session then waits forever instead of failing.

Locally the claude-code case also timed out about one run in three. That was
not the transport: c3_session(save) ran auto_memory.on_session_end() and the
distiller against the live Ollama on this box, and a cold gemma3 load takes
longer than the 30 s read timeout. CI has no Ollama, so it never saw it. The
test project now points ollama_base_url at a dead port. Each host case also
runs in its own interpreter, the way real clients do.

Claude-Session: https://claude.ai/code/session_0165dDtca3R3ggdkX4rAPJoP
windows-latest / py3.12 timed out at 10 s: cmd.exe -> powershell.exe -> python
cold start on a busy runner, not the hook (the stub script exits at once).
The other two Windows cells passed the same test.

Claude-Session: https://claude.ai/code/session_0165dDtca3R3ggdkX4rAPJoP
… on one target

tests/test_atomic_json.py::TestConcurrency failed on windows-latest in two
of four CI runs of PR #159 with PermissionError(13). The 2.118.2 helper
retried os.replace four times across 0.14 s, sized for an AV scanner or the
Search indexer holding a handle. Six threads replacing the same destination
on a loaded runner need longer: two concurrent MoveFileEx calls on one
target can also answer WinError 5, and the loser waits for its turn.

Eight attempts across ~2.5 s of doubling backoff. Only the failure path
pays; a failure that survives it is still re-raised.

Claude-Session: https://claude.ai/code/session_0165dDtca3R3ggdkX4rAPJoP
@drknowhow
drknowhow merged commit a282b3a into main Sep 5, 2026
12 checks passed
@drknowhow
drknowhow deleted the feat/codex-native branch September 5, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant