Skip to content

Commit bd665ea

Browse files
committed
Validate Codex runtime option compatibility
1 parent 87f3b47 commit bd665ea

9 files changed

Lines changed: 460 additions & 32 deletions

File tree

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@ The plugin lets Claude Code launch one Codex agent or several Codex agents in pa
1414
- Service tier: omitted by default so Codex uses its normal account/default service tier. Pass `service_tier` only when you explicitly want one.
1515
- Transport: stdio MCP, launched by Claude Code for the active session. No daemon is required.
1616
- Prompt delivery: stdin, not command-line arguments.
17+
- Codex home: uses the user's Codex home by default; pass `isolated_codex_home: true` to use a temporary Codex home with auth but without inherited `config.toml` MCP servers.
1718

1819
Optional environment overrides:
1920

2021
- `CODEX_SUBAGENTS_CODEX_BIN`: explicit Codex CLI path.
2122
- `CODEX_SUBAGENTS_DEFAULT_MODEL`: model to use when a tool call omits `model`.
22-
- `CODEX_SUBAGENTS_DEFAULT_REASONING_EFFORT`: `minimal`, `low`, `medium`, `high`, or `xhigh`.
23+
- `CODEX_SUBAGENTS_DEFAULT_REASONING_EFFORT`: `low`, `medium`, `high`, or `xhigh`. `minimal` is ignored as a default and falls back to `medium`.
2324

2425
## Spark And Nested Subagents
2526

2627
Use `model_preset: "spark"` to launch a top-level Codex agent with `gpt-5.3-codex-spark`. Exact `model` still wins when both are provided.
2728

29+
Spark does not support `reasoning_summary`; the plugin rejects `model_preset: "spark"` with `reasoning_summary` values other than `none` before starting Codex.
30+
31+
`reasoning_effort: "minimal"` is also rejected before starting Codex because the current Codex CLI auto-attaches `web_search`, which the API does not allow with minimal reasoning. Use `low` or higher.
32+
2833
To let a Codex agent spawn its own Codex subagents, pass:
2934

3035
- `codex_subagents`: custom Codex agent definitions with `name`, `description`, `developer_instructions`, optional `model` or `model_preset`, reasoning effort, sandbox, MCP servers, skills config, and extra config.
@@ -88,7 +93,7 @@ After startup, ask Claude to use Codex subagents, or invoke the plugin skill:
8893

8994
`codex_status` reports the resolved Codex binary, server working directory, Claude project directory, default model, default reasoning effort, and version probe.
9095

91-
Each agent accepts model, reasoning effort, sandbox, project directory, timeout, and output-size controls. Pass `project_dir` when Claude Code wants Codex to inspect the same repository or subdirectory Claude is currently working in. If `project_dir` is omitted, the server uses `CLAUDE_PROJECT_DIR` when Claude Code provides it. Omit model to use Codex's configured default or the plugin's optional configured default model.
96+
Each agent accepts model, reasoning effort, sandbox, project directory, timeout, isolated Codex home, and output-size controls. Pass `project_dir` when Claude Code wants Codex to inspect the same repository or subdirectory Claude is currently working in. If `project_dir` is omitted, the server uses `CLAUDE_PROJECT_DIR` when Claude Code provides it. Omit model to use Codex's configured default or the plugin's optional configured default model.
9297

9398
## License
9499

dist/index.js

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21317,17 +21317,22 @@ async function linkIfPresent(source, destination) {
2131721317
if (!await exists(source)) return;
2131821318
await symlink(source, destination);
2131921319
}
21320-
async function prepareTempCodexHome(definitions, env) {
21320+
async function prepareTempCodexHome(definitions, env, options = {}) {
2132121321
const realCodexHome = env.CODEX_HOME?.trim() || path2.join(os2.homedir(), ".codex");
2132221322
const tempCodexHome = await mkdtemp(path2.join(os2.tmpdir(), "codex-subagents-home-"));
21323-
await Promise.all([
21323+
const links = [
2132421324
linkIfPresent(path2.join(realCodexHome, "auth.json"), path2.join(tempCodexHome, "auth.json")),
21325-
linkIfPresent(path2.join(realCodexHome, "config.toml"), path2.join(tempCodexHome, "config.toml")),
2132621325
linkIfPresent(path2.join(realCodexHome, "AGENTS.md"), path2.join(tempCodexHome, "AGENTS.md")),
2132721326
linkIfPresent(path2.join(realCodexHome, "skills"), path2.join(tempCodexHome, "skills")),
2132821327
linkIfPresent(path2.join(realCodexHome, "rules"), path2.join(tempCodexHome, "rules")),
2132921328
linkIfPresent(path2.join(realCodexHome, "plugins"), path2.join(tempCodexHome, "plugins"))
21330-
]);
21329+
];
21330+
if (options.isolated) {
21331+
links.push(writeFile(path2.join(tempCodexHome, "config.toml"), "# isolated codex-subagents run\n"));
21332+
} else {
21333+
links.push(linkIfPresent(path2.join(realCodexHome, "config.toml"), path2.join(tempCodexHome, "config.toml")));
21334+
}
21335+
await Promise.all(links);
2133121336
const agentsDir = path2.join(tempCodexHome, "agents");
2133221337
await mkdir(agentsDir, { recursive: true });
2133321338
await Promise.all(
@@ -21375,8 +21380,10 @@ async function prepareSubagents(options) {
2137521380
const tasks = options.tasks ?? [];
2137621381
const env = { ...options.env ?? {} };
2137721382
let tempCodexHome;
21378-
if (definitions.length > 0) {
21379-
tempCodexHome = await prepareTempCodexHome(definitions, { ...process.env, ...env });
21383+
if (definitions.length > 0 || options.isolatedCodexHome) {
21384+
tempCodexHome = await prepareTempCodexHome(definitions, { ...process.env, ...env }, {
21385+
isolated: options.isolatedCodexHome
21386+
});
2138021387
env.CODEX_HOME = tempCodexHome;
2138121388
}
2138221389
return {
@@ -21396,6 +21403,7 @@ var sandboxModes = ["read-only", "workspace-write", "danger-full-access"];
2139621403
var serviceTiers = ["fast", "flex"];
2139721404
var modelVerbosities = ["low", "medium", "high"];
2139821405
var reasoningSummaries = ["auto", "concise", "detailed", "none"];
21406+
var sparkModel = "gpt-5.3-codex-spark";
2139921407
var LimitedText = class {
2140021408
constructor(maxChars) {
2140121409
this.maxChars = maxChars;
@@ -21422,7 +21430,7 @@ var LimitedText = class {
2142221430
};
2142321431
function defaultReasoningEffort(env = process.env) {
2142421432
const value = env.CODEX_SUBAGENTS_DEFAULT_REASONING_EFFORT?.trim();
21425-
return reasoningEfforts.includes(value) ? value : "medium";
21433+
return value !== "minimal" && reasoningEfforts.includes(value) ? value : "medium";
2142621434
}
2142721435
function defaultModel(env = process.env) {
2142821436
const value = env.CODEX_SUBAGENTS_DEFAULT_MODEL?.trim();
@@ -21432,6 +21440,32 @@ function defaultModel(env = process.env) {
2143221440
function resolveRequestedModel(options, env = process.env) {
2143321441
return options.model?.trim() || modelForPreset(options.modelPreset) || defaultModel(env);
2143421442
}
21443+
var RunValidationError = class extends Error {
21444+
constructor(message) {
21445+
super(message);
21446+
this.name = "RunValidationError";
21447+
}
21448+
};
21449+
function validateRunConfiguration(options, env = process.env) {
21450+
const model = resolveRequestedModel(options, env);
21451+
const reasoningEffort = options.reasoningEffort ?? defaultReasoningEffort(env);
21452+
let reasoningSummary = options.reasoningSummary;
21453+
if (reasoningEffort === "minimal") {
21454+
throw new RunValidationError(
21455+
"reasoning_effort='minimal' is not supported by this plugin because Codex currently auto-attaches web_search, which the API rejects with reasoning.effort 'minimal'. Use reasoning_effort='low' or higher."
21456+
);
21457+
}
21458+
if (model === sparkModel && reasoningSummary) {
21459+
if (reasoningSummary === "none") {
21460+
reasoningSummary = void 0;
21461+
} else {
21462+
throw new RunValidationError(
21463+
`reasoning_summary='${reasoningSummary}' is not supported with model_preset='spark' (${sparkModel}). Omit reasoning_summary or use reasoning_summary='none'.`
21464+
);
21465+
}
21466+
}
21467+
return { model, reasoningEffort, reasoningSummary };
21468+
}
2143521469
async function resolveWorkingDirectory(cwd, env = process.env) {
2143621470
const requested = cwd?.trim();
2143721471
const claudeProjectDir = env.CLAUDE_PROJECT_DIR?.trim();
@@ -21447,8 +21481,7 @@ function tomlString(value) {
2144721481
return JSON.stringify(value);
2144821482
}
2144921483
function buildCodexExecArgs(options, outputPath, env = process.env) {
21450-
const model = resolveRequestedModel(options, env);
21451-
const reasoningEffort = options.reasoningEffort ?? defaultReasoningEffort(env);
21484+
const { model, reasoningEffort, reasoningSummary } = validateRunConfiguration(options, env);
2145221485
const sandbox = options.sandbox ?? "read-only";
2145321486
const ephemeral = options.ephemeral ?? true;
2145421487
const args = [
@@ -21474,8 +21507,8 @@ function buildCodexExecArgs(options, outputPath, env = process.env) {
2147421507
if (options.modelVerbosity) {
2147521508
args.push("-c", `model_verbosity=${tomlString(options.modelVerbosity)}`);
2147621509
}
21477-
if (options.reasoningSummary) {
21478-
args.push("-c", `model_reasoning_summary=${tomlString(options.reasoningSummary)}`);
21510+
if (reasoningSummary) {
21511+
args.push("-c", `model_reasoning_summary=${tomlString(reasoningSummary)}`);
2147921512
}
2148021513
if (options.serviceTier) {
2148121514
args.push("-c", `service_tier=${tomlString(options.serviceTier)}`);
@@ -21495,6 +21528,45 @@ function buildCodexExecArgs(options, outputPath, env = process.env) {
2149521528
args.push("-");
2149621529
return args;
2149721530
}
21531+
function validationFailureResult(options) {
21532+
const reasoningEffort = options.runOptions.reasoningEffort ?? defaultReasoningEffort(options.env);
21533+
const message = options.error.message;
21534+
return {
21535+
name: options.runOptions.name,
21536+
ok: false,
21537+
status: "failed",
21538+
durationMs: Date.now() - options.started,
21539+
codexBinary: options.codexBinary,
21540+
cwd: options.cwd,
21541+
model: resolveRequestedModel(options.runOptions, options.env),
21542+
modelPreset: options.runOptions.modelPreset,
21543+
reasoningEffort,
21544+
sandbox: options.runOptions.sandbox ?? "read-only",
21545+
serviceTier: options.runOptions.serviceTier,
21546+
exitCode: null,
21547+
signal: null,
21548+
finalMessage: "",
21549+
stderr: message,
21550+
stdoutTail: "",
21551+
truncated: {
21552+
stdoutChars: 0,
21553+
stderrChars: 0,
21554+
finalMessageChars: 0
21555+
},
21556+
eventSummary: {
21557+
counts: {},
21558+
commands: [],
21559+
errors: [message]
21560+
},
21561+
commandPreview: [],
21562+
validationError: message,
21563+
codexSubagents: {
21564+
customAgents: options.runOptions.codexSubagents?.map((agent) => agent.name) ?? [],
21565+
requestedTasks: options.runOptions.subagentTasks?.length ?? 0,
21566+
tempCodexHomeUsed: false
21567+
}
21568+
};
21569+
}
2149821570
function parseJsonLine(line, summary) {
2149921571
if (!line.trim()) return;
2150021572
let event;
@@ -21544,10 +21616,26 @@ async function runAgent(options) {
2154421616
explicitPath: options.codexBin,
2154521617
env: mergedEnv
2154621618
});
21619+
try {
21620+
validateRunConfiguration(options, mergedEnv);
21621+
} catch (error2) {
21622+
if (error2 instanceof RunValidationError) {
21623+
return validationFailureResult({
21624+
started,
21625+
error: error2,
21626+
codexBinary,
21627+
cwd,
21628+
runOptions: options,
21629+
env: mergedEnv
21630+
});
21631+
}
21632+
throw error2;
21633+
}
2154721634
const preparedSubagents = await prepareSubagents({
2154821635
definitions: options.codexSubagents,
2154921636
tasks: options.subagentTasks,
21550-
env: options.env
21637+
env: options.env,
21638+
isolatedCodexHome: options.isolatedCodexHome
2155121639
});
2155221640
const tempDir = await mkdtemp2(path3.join(os3.tmpdir(), "codex-subagents-"));
2155321641
const childEnv = { ...mergedEnv, ...preparedSubagents.env };
@@ -21718,9 +21806,11 @@ var usageGuide = [
2171821806
"- Keep sandbox read-only unless the user explicitly asks for a different sandbox.",
2171921807
"- Approvals are non-interactive; do not expect Codex to ask permission.",
2172021808
'- Prefer model_preset "spark" for responsive focused checks, small reviews, UI iteration, and sidecar analysis.',
21721-
'- Use reasoning_effort "medium" by default, "low" for simple checks, and "high" or "xhigh" only for difficult analysis.',
21809+
'- Use reasoning_effort "medium" by default, "low" for simple checks, and "high" or "xhigh" only for difficult analysis. Do not use "minimal"; Codex currently auto-attaches web_search and the API rejects that tool with minimal reasoning.',
21810+
'- Do not combine model_preset "spark" with reasoning_summary values other than "none"; Spark does not support reasoning.summary.',
2172221811
"- Do not set service_tier by default. Let Codex use its normal account/default service tier unless the user explicitly asks for a service tier.",
2172321812
"- Pass project_dir whenever Claude knows the active project directory so Codex works in the same tree as Claude Code.",
21813+
"- Set isolated_codex_home true when a run should ignore the user's Codex MCP server config and use only this request's temporary Codex configuration.",
2172421814
"- Ask Codex for concise results with file paths, line references, and actionable findings when reviewing code.",
2172521815
"",
2172621816
"Nested Codex subagents:",
@@ -21774,14 +21864,16 @@ var commonInputSchema = {
2177421864
"Convenience model preset. Use `spark` for responsive Codex Spark work; it maps to gpt-5.3-codex-spark."
2177521865
),
2177621866
reasoning_effort: reasoningEffortSchema.optional().describe(
21777-
"Codex model reasoning effort. Prefer medium by default, low for simple checks, high/xhigh only for difficult analysis."
21867+
"Codex model reasoning effort. Prefer medium by default, low for simple checks, high/xhigh only for difficult analysis. `minimal` is rejected because Codex currently auto-attaches web_search, which the API does not allow with minimal reasoning."
2177821868
),
2177921869
sandbox: sandboxModeSchema.default("read-only").describe("Codex sandbox mode. Keep read-only unless the user explicitly asks otherwise."),
2178021870
service_tier: serviceTierSchema.optional().describe(
2178121871
"Optional Codex service tier. Omit by default; only set this when the user explicitly asks for a service tier."
2178221872
),
2178321873
model_verbosity: modelVerbositySchema.optional().describe("Optional GPT-5 model verbosity override."),
21784-
reasoning_summary: reasoningSummarySchema.optional().describe("Optional Codex reasoning summary setting."),
21874+
reasoning_summary: reasoningSummarySchema.optional().describe(
21875+
"Optional Codex reasoning summary setting. Do not use with model_preset `spark` except `none`; Spark does not support reasoning.summary."
21876+
),
2178521877
cwd: external_exports.string().trim().min(1).optional().describe("Compatibility alias for project_dir."),
2178621878
project_dir: external_exports.string().trim().min(1).optional().describe(
2178721879
"Project directory for Codex. Pass Claude Code's active project directory so Codex works in the same tree. Defaults to CLAUDE_PROJECT_DIR when Claude provides it."
@@ -21794,6 +21886,9 @@ var commonInputSchema = {
2179421886
ephemeral: external_exports.boolean().default(true).describe("Run Codex without persisting session rollout files."),
2179521887
skip_git_repo_check: external_exports.boolean().default(false).describe("Allow Codex to run outside a Git repository."),
2179621888
ignore_rules: external_exports.boolean().default(false).describe("Skip Codex execpolicy .rules files."),
21889+
isolated_codex_home: external_exports.boolean().default(false).describe(
21890+
"Run with a temporary Codex home that links auth but does not inherit the user's Codex config.toml. Use to avoid unrelated MCP servers from the user's Codex config."
21891+
),
2179721892
codex_subagents: external_exports.array(codexSubagentSchema).max(24).optional().describe(
2179821893
"Complete custom Codex subagent definitions available inside this Codex run for nested delegation."
2179921894
),
@@ -21850,6 +21945,7 @@ function toRunOptions(args) {
2185021945
ephemeral: args.ephemeral,
2185121946
skipGitRepoCheck: args.skip_git_repo_check,
2185221947
ignoreRules: args.ignore_rules,
21948+
isolatedCodexHome: args.isolated_codex_home,
2185321949
codexSubagents: toCodexSubagents(args.codex_subagents),
2185421950
subagentTasks: args.subagent_tasks,
2185521951
subagentRuntime: args.subagent_runtime ? {
@@ -21950,6 +22046,7 @@ var parallelAgentSchema = external_exports.object({
2195022046
ephemeral: commonInputSchema.ephemeral.optional(),
2195122047
skip_git_repo_check: commonInputSchema.skip_git_repo_check.optional(),
2195222048
ignore_rules: commonInputSchema.ignore_rules.optional(),
22049+
isolated_codex_home: commonInputSchema.isolated_codex_home.optional(),
2195322050
codex_subagents: commonInputSchema.codex_subagents,
2195422051
subagent_tasks: commonInputSchema.subagent_tasks,
2195522052
subagent_runtime: commonInputSchema.subagent_runtime
@@ -21994,6 +22091,7 @@ server.registerTool(
2199422091
ephemeral: agent.ephemeral ?? args.ephemeral,
2199522092
skipGitRepoCheck: agent.skip_git_repo_check ?? args.skip_git_repo_check,
2199622093
ignoreRules: agent.ignore_rules ?? args.ignore_rules,
22094+
isolatedCodexHome: agent.isolated_codex_home ?? args.isolated_codex_home,
2199722095
codexSubagents: toCodexSubagents(agent.codex_subagents ?? args.codex_subagents),
2199822096
subagentTasks: agent.subagent_tasks ?? args.subagent_tasks,
2199922097
subagentRuntime: agent.subagent_runtime ? {

skills/codex-subagents/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@ For independent tasks that can run concurrently, call `run_agents` with one agen
2222

2323
When Claude wants Codex to work in the same repository or folder as the active Claude Code session, pass that folder as `project_dir`. Use `cwd` only as a compatibility alias.
2424

25-
Prefer `reasoning_effort: "medium"` for exploration and `high` or `xhigh` only when the task is complex enough to justify the extra latency and token usage.
25+
Prefer `reasoning_effort: "medium"` for exploration and `high` or `xhigh` only when the task is complex enough to justify the extra latency and token usage. Do not use `minimal`; the plugin rejects it because Codex currently auto-attaches `web_search`, which the API does not allow with minimal reasoning.
2626

2727
Use `model_preset: "spark"` for responsive, focused work such as UI iteration, narrow exploration, small reviews, and quick sidecar checks.
2828

29+
Do not set `reasoning_summary` with `model_preset: "spark"` except for `reasoning_summary: "none"`. Spark does not support `reasoning.summary`, and the plugin rejects unsupported combinations before starting Codex.
30+
2931
Do not set `service_tier` by default. Let Codex use its normal account/default service tier unless the user explicitly asks for a service tier.
3032

33+
Set `isolated_codex_home: true` when unrelated Codex MCP servers from the user's `~/.codex/config.toml` should not be loaded for the run.
34+
3135
Use `codex_status` only when diagnosing installation or binary resolution, or after a failed Codex tool call. Normal delegation should start with `run_agent` or `run_agents`.
3236

3337
Example single-agent call:

0 commit comments

Comments
 (0)