Skip to content

feat: let Claude Code agents use OpenAI models#72

Open
tnsqjahong wants to merge 6 commits intoopenai:mainfrom
tnsqjahong:main
Open

feat: let Claude Code agents use OpenAI models#72
tnsqjahong wants to merge 6 commits intoopenai:mainfrom
tnsqjahong:main

Conversation

@tnsqjahong
Copy link
Copy Markdown

Summary

Let any Claude Code agent run on OpenAI models (e.g. gpt-5.4, gpt-5.3-codex) through the existing Codex companion runtime.

  • /codex:agents — interactive TUI for per-agent model switching (Claude ↔ OpenAI)
  • /codex:setup now asks about agent model configuration after init
  • Patched agents reuse the exact same thin-forwarder pattern as codex-rescue — single Bash call to codex-companion.mjs task, so Anthropic quota stays near zero
  • Dynamic model list sourced from codex-companion aliases + ~/.codex/config.toml

How it works

When an agent is switched to an OpenAI model:

  1. Frontmatter gets model: haiku, tools: Bash, _codex_model: <model>
  2. Body is replaced with the codex-rescue forwarding prompt + the original role context
  3. At spawn time, haiku makes one Bash call → codex-companion.mjs task --model <model> → OpenAI handles the work
  4. Switching back to a Claude model removes the forwarder and restores the original body

The TUI uses a TTY hack (C input proxy via tcsetpgrp) to render an interactive ↑↓ navigable list directly inside Claude Code's terminal.

Files

File Purpose
commands/agents.md /codex:agents slash command
commands/setup.md Added agent config prompt after setup
hooks/hooks.json SessionStart hook for auto-patching
scripts/patch-agents.mjs Model switching engine
scripts/agent-config-tui.mjs Interactive TUI
scripts/input_proxy.c C proxy for TTY capture (auto-compiled)
scripts/auto-patch-agents-hook.mjs SessionStart auto-patcher

Test plan

  • Codex agents respond via GPT-5.4 (tool_uses: 1, strict forwarding)
  • Claude agents continue to work normally
  • Model switching cycle: opus → gpt-5.4 → sonnet → gpt-5.3-codex → opus — clean round-trip
  • disallowedTools preserved through patch/restore
  • Dynamic model list picks up codex-companion aliases and config.toml
  • TUI renders inside Claude Code terminal with ↑↓ navigation

🤖 Generated with Claude Code

Allow any Claude Code agent to run on OpenAI models (e.g. gpt-5.4)
through the existing Codex companion runtime. Patched agents use the
same thin-forwarder pattern as codex-rescue (single Bash call to
codex-companion.mjs task), so Anthropic quota stays near zero.

New:
- /codex:agents — interactive TUI for per-agent model switching
- /codex:setup now asks about agent model configuration after init
- SessionStart hook auto-patches agents set to OpenAI models
- Dynamic model list from codex-companion aliases + ~/.codex/config.toml
- C input proxy for TTY capture in the TUI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tnsqjahong tnsqjahong requested a review from a team March 31, 2026 16:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb3499271a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

function removeField(raw, key) {
return raw.replace(new RegExp(`^${key}:.*\r?\n`, "m"), "").replace(/\n{2,}/g, "\n");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove terminal frontmatter keys when restoring Claude mode

The removeField regex only matches keys that end with a newline, so _codex_model is not removed when it is the last frontmatter line (which this patcher itself creates). In that case, switching an agent back to haiku/sonnet/opus leaves _codex_model behind, and listAgents() will still report the old Codex model because it prioritizes _codex_model over model, so round-tripping models via /codex:agents can get stuck in an inconsistent state.

Useful? React with 👍 / 👎.

Comment on lines +114 to +116
const re = new RegExp(`^${key}:.*$`, "m");
if (re.test(raw)) {
return raw.replace(re, `${key}: ${value}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve YAML list fields when rewriting frontmatter

This replacement logic only rewrites the first tools: line and does not remove any following indented list items, so agents that declare tools as a YAML sequence (for example tools:\n - Bash) are corrupted into invalid frontmatter after patching (tools: Bash plus dangling - ... lines). That can break loading/parsing of affected agents as soon as they are switched to a Codex model.

Useful? React with 👍 / 👎.

- setField now replaces indented continuation lines (e.g. tools:\n  - Bash)
- removeField handles last-line keys without trailing newline

Addresses Codex review feedback on PR openai#72.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f31d4167e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Switching to Claude model — remove codex fields
let newRaw = setField(raw, "model", targetModel);
newRaw = removeField(newRaw, "_codex_model");
newRaw = removeField(newRaw, "tools");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve agent tool config when switching to Claude

Do not unconditionally delete tools in the Claude-model path: this branch runs for any patch <agent> <haiku|sonnet|opus> call, including normal Claude→Claude switches, so agents with explicit tool constraints lose them permanently after a model change. In practice, an agent configured with a YAML tools list (or a restricted tool set) will come back without tools, changing its permissions/behavior and breaking round-trips after experimenting with OpenAI models.

Useful? React with 👍 / 👎.

Claude-to-Claude model switches (e.g. sonnet→opus) no longer strip
the agent's tools config. The cleanup only runs when _codex_model
was present (i.e. the agent was in Codex forwarder mode).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 576d51a0b0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


try {
const patchScript = path.join(SCRIPT_DIR, "patch-agents.mjs");
execSync(`node "${patchScript}" patch "${agentName}" "${codexModel}"`, { timeout: 5000 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid shell interpolation for auto-patch arguments

The hook executes execSync with agentName and codexModel interpolated into a shell string, but both values come from agent file contents/filenames. A model value containing a quote (for example model: gpt-5.4"; ...) breaks out of the quoted argument and runs arbitrary shell commands during SessionStart. This is a command-injection path in a hook that runs automatically, so arguments should be passed without invoking a shell.

Useful? React with 👍 / 👎.

Comment on lines +221 to +223
const disallowed = (fields.disallowedTools || "").toLowerCase();
const readOnly = disallowed.includes("write") || disallowed.includes("edit");
const writeFlag = readOnly ? "" : " --write";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect YAML disallowedTools lists before enabling --write

The write-mode decision relies on fields.disallowedTools, but the frontmatter parser only keeps single-line key: value entries. For common YAML list syntax (disallowedTools: followed by indented - Write/- Edit), fields.disallowedTools is empty, so patched agents are treated as writable and the generated forwarder includes --write even when write/edit tools were explicitly disallowed.

Useful? React with 👍 / 👎.

- auto-patch-agents-hook: use execFileSync instead of execSync to
  avoid shell interpolation of agent names and model values
- parseFrontmatter: handle indented YAML list items (e.g.
  disallowedTools:\n  - Write\n  - Edit) so --write flag is
  correctly determined

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4abbf3294a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const patchScript = path.join(SCRIPT_DIR, "patch-agents.mjs");
for (const [filePath, { name, model }] of entries) {
try {
execSync(`node "${patchScript}" patch "${filePath}" "${model}"`, { encoding: "utf-8", timeout: 5000 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass patch arguments without invoking a shell

The apply-changes path interpolates filePath and model into an execSync shell string, but both values are loaded from agent metadata (list --json) and are not escaped. A crafted agent filename or model alias containing quotes/shell metacharacters can break argument boundaries and execute arbitrary commands when a user exits the TUI with changes. Use a non-shell invocation (execFileSync/spawn with argv) so these values are passed as literal arguments.

Useful? React with 👍 / 👎.

Comment on lines +230 to +232
const disallowed = (fields.disallowedTools || "").toLowerCase();
const readOnly = disallowed.includes("write") || disallowed.includes("edit");
const writeFlag = readOnly ? "" : " --write";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive Codex write mode from tools allowlist

Read-only detection only checks disallowedTools, so agents that are effectively read-only via their tools allowlist (e.g., tools: Read or a list without Write/Edit) are still treated as writable and get --write in the generated forwarder. In that scenario, switching to an OpenAI model silently broadens permissions and allows Codex file edits that the original agent configuration did not permit. The write-mode decision should also honor the tools field.

Useful? React with 👍 / 👎.

- agent-config-tui: use execFileSync to avoid shell interpolation
- patch-agents: check tools allowlist for Write/Edit before adding
  --write flag (e.g. tools: Read, Glob means read-only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b669c6dd1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

function isOpenAIModel(model) {
return model && !CLAUDE_MODELS.has(model.toLowerCase());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize quoted model names before OpenAI detection

The SessionStart hook treats any model string not in CLAUDE_MODELS as OpenAI, but parseFrontmatter keeps YAML quotes, so a valid frontmatter value like model: "sonnet" is interpreted as "sonnet" and fails the Claude-model check. In that case the hook will auto-patch a normal Claude agent into the Codex forwarder on every session start, which silently changes runtime/provider behavior for users who quote model values.

Useful? React with 👍 / 👎.

Comment on lines +163 to +167
for (const { dir, source } of allDirs) {
const filePath = path.join(dir, `${name}.md`);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, "utf-8");
found = { filePath, source, content, ...parseFrontmatter(content) };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disambiguate patch targets when agent names collide

readAgent keeps overwriting found as it scans directories and returns the last non-user match, so patch <name> <model> is ambiguous whenever multiple plugin sources contain the same <name>.md. Because list --json can return duplicate names across sources, name-based patching can apply the change to the wrong plugin agent (depending on directory traversal order) instead of the one the user selected.

Useful? React with 👍 / 👎.

parseFrontmatter and auto-patch hook now strip surrounding quotes
from values (e.g. model: "sonnet" → sonnet), preventing quoted
Claude model names from being misidentified as OpenAI models.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 636591ccd4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +233 to +236
const hasToolsList = tools.length > 0;
const toolsAllowWrite = !hasToolsList || tools.includes("write") || tools.includes("edit");
const readOnly = disallowed.includes("write") || disallowed.includes("edit") || !toolsAllowWrite;
const writeFlag = readOnly ? "" : " --write";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject Codex patch when Bash is disallowed

The forwarder prompt always requires a Bash invocation, but this gating logic only inspects write/edit restrictions. If an agent has disallowedTools containing Bash (including YAML list form), patch still converts it to Codex mode and keeps the disallow in frontmatter, leaving an agent that cannot execute its only required tool call. This makes the switched agent non-functional; the patch path should fail fast (or explicitly reconcile disallowedTools) when Bash is blocked.

Useful? React with 👍 / 👎.

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