feat: let Claude Code agents use OpenAI models#72
feat: let Claude Code agents use OpenAI models#72tnsqjahong wants to merge 6 commits intoopenai:mainfrom
Conversation
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>
There was a problem hiding this comment.
💡 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"); |
There was a problem hiding this comment.
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 👍 / 👎.
| const re = new RegExp(`^${key}:.*$`, "m"); | ||
| if (re.test(raw)) { | ||
| return raw.replace(re, `${key}: ${value}`); |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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"); |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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 }); |
There was a problem hiding this comment.
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 👍 / 👎.
| const disallowed = (fields.disallowedTools || "").toLowerCase(); | ||
| const readOnly = disallowed.includes("write") || disallowed.includes("edit"); | ||
| const writeFlag = readOnly ? "" : " --write"; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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 }); |
There was a problem hiding this comment.
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 👍 / 👎.
| const disallowed = (fields.disallowedTools || "").toLowerCase(); | ||
| const readOnly = disallowed.includes("write") || disallowed.includes("edit"); | ||
| const writeFlag = readOnly ? "" : " --write"; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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()); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) }; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| 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"; |
There was a problem hiding this comment.
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 👍 / 👎.
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:setupnow asks about agent model configuration after initcodex-rescue— singleBashcall tocodex-companion.mjs task, so Anthropic quota stays near zerocodex-companionaliases +~/.codex/config.tomlHow it works
When an agent is switched to an OpenAI model:
model: haiku,tools: Bash,_codex_model: <model>codex-companion.mjs task --model <model>→ OpenAI handles the workThe TUI uses a TTY hack (C input proxy via
tcsetpgrp) to render an interactive ↑↓ navigable list directly inside Claude Code's terminal.Files
commands/agents.md/codex:agentsslash commandcommands/setup.mdhooks/hooks.jsonscripts/patch-agents.mjsscripts/agent-config-tui.mjsscripts/input_proxy.cscripts/auto-patch-agents-hook.mjsTest plan
tool_uses: 1, strict forwarding)opus → gpt-5.4 → sonnet → gpt-5.3-codex → opus— clean round-tripdisallowedToolspreserved through patch/restorecodex-companionaliases andconfig.toml🤖 Generated with Claude Code