From 2767a704977216ad627483d6ec80075b3475732b Mon Sep 17 00:00:00 2001 From: anessbelbati <102622182+anessbelbati@users.noreply.github.com> Date: Wed, 7 Oct 2026 04:48:44 +0100 Subject: [PATCH] Keep Claude's MCP approval keys out of the Codex hooks file Setup wrote the same JSON to .claude/settings.json and .codex/hooks.json, including enableAllProjectMcpServers and enabledMcpjsonServers. Codex accepts only "description" and "hooks" at the top of a hooks file, and with any other key it loads no hooks from the file at all: failed to parse hooks config .../.codex/hooks.json: unknown field `enableAllProjectMcpServers`, expected `description` or `hooks` at line 2 column 30 So the context hook never ran under Codex, and every prompt went out without the Video FS context. Measured with Codex 0.160.1 against a stand-in model server, on macOS and on Linux: 0 hooks loaded from the file as written and no context in the model request; with the two keys gone, 1 hook loaded, it ran, and the context was in the request. The two keys now go into Claude's file only. Setup also removes them from a .codex/hooks.json that is the desktop's own earlier output, under the same ownership test enableProjectMcpServers already uses, so an existing project recovers the next time it is opened. A file the user wrote is left alone. --- desktop/agent-setup.mjs | 45 ++++++++++++++++++++--- desktop/agent-setup.test.mjs | 43 ++++++++++++++++++++++ desktop/codex-hook-compatibility.test.mjs | 2 + 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/desktop/agent-setup.mjs b/desktop/agent-setup.mjs index ed572f5..7f2c3de 100644 --- a/desktop/agent-setup.mjs +++ b/desktop/agent-setup.mjs @@ -72,8 +72,8 @@ export async function setupAgentProject({ projectId: rawProjectId, state }) { `args = ${tomlStringArray(launcher.args)}`, "", ].join("\n"); - const claudeHookConfig = hookConfig(claudeHookLauncher); - const codexHookConfig = hookConfig(codexHookLauncher); + const claudeHookConfig = hookConfig(claudeHookLauncher, "claude"); + const codexHookConfig = hookConfig(codexHookLauncher, "codex"); const guide = agentGuide(projectId, codexProjectRoot); for (const output of [ @@ -109,6 +109,7 @@ export async function setupAgentProject({ projectId: rawProjectId, state }) { claudeHookConfig, ); await enableProjectMcpServers(path.join(projectRoot, ".claude", "settings.json")); + await removeClaudeKeysFromCodexHooks(path.join(projectRoot, ".codex", "hooks.json")); const codexHookResult = await writePrivateFileIfAbsent( path.join(projectRoot, ".codex", "hooks.json"), codexHookConfig, @@ -293,7 +294,7 @@ function contextHookLauncherForProject(mcp, projectId, agent) { }; } -function hookConfig(launcher) { +function hookConfig(launcher, agent) { const command = [launcher.command, ...launcher.args] .map(shellQuote) .join(" "); @@ -303,8 +304,14 @@ function hookConfig(launcher) { // interactive "approve this MCP server" prompt — the desktop wrote that // config itself, so approval is implicit. Current Claude releases need // the explicit server allowlist even when the blanket legacy flag is set. - enableAllProjectMcpServers: true, - enabledMcpjsonServers: ["video-fs"], + // Codex loads no hooks at all from a file with a top-level key it does + // not know, so these stay out of its file. + ...(agent === "claude" + ? { + enableAllProjectMcpServers: true, + enabledMcpjsonServers: ["video-fs"], + } + : {}), hooks: { UserPromptSubmit: [ { @@ -362,6 +369,34 @@ async function enableProjectMcpServers(settingsPath) { } } +/** Earlier desktop output put Claude's two MCP approval keys into the Codex + * hooks file as well, and Codex loads no hooks from a file with a top-level + * key it does not know. Remove them ONLY from our own earlier output, under + * the same test as above. */ +async function removeClaudeKeysFromCodexHooks(hooksPath) { + try { + const parsed = JSON.parse(await readFile(hooksPath, "utf8")); + if (!parsed || typeof parsed !== "object") return; + const keys = Object.keys(parsed); + const appOwned = + keys.every( + (key) => + key === "hooks" || + key === "enableAllProjectMcpServers" || + key === "enabledMcpjsonServers", + ) && + JSON.stringify(parsed.hooks ?? {}).includes("--agent-context-hook"); + if (!appOwned || keys.every((key) => key === "hooks")) return; + await writeFile( + hooksPath, + `${JSON.stringify({ hooks: parsed.hooks }, null, 2)}\n`, + { mode: 0o600 }, + ); + } catch { + // Missing or hand-mangled hooks stay untouched. + } +} + async function writePrivateFileIfAbsent(filePath, contents) { const existing = await readFile(filePath, "utf8").catch((error) => { if (error?.code === "ENOENT") return null; diff --git a/desktop/agent-setup.test.mjs b/desktop/agent-setup.test.mjs index 95bf0b8..2b0a74e 100644 --- a/desktop/agent-setup.test.mjs +++ b/desktop/agent-setup.test.mjs @@ -179,6 +179,49 @@ test("legacy app-owned Claude settings gain the explicit Video FS approval", asy assert.deepEqual(settings.enabledMcpjsonServers, ["video-fs"]); }); +test("legacy app-owned Codex hooks lose the Claude keys that make Codex reject the file", async () => { + const input = await fixture("legacy-codex-hooks"); + const hooks = { + UserPromptSubmit: [ + { + hooks: [ + { + command: + "'/Applications/Video FS.app/Contents/MacOS/Video FS' --agent-context-hook --agent codex --project-id legacy-codex-hooks", + timeout: 5, + type: "command", + }, + ], + }, + ], + }; + await mkdir(path.join(input.projectRoot, ".codex")); + await writeFile( + path.join(input.projectRoot, ".codex", "hooks.json"), + `${JSON.stringify( + { + enableAllProjectMcpServers: true, + enabledMcpjsonServers: ["video-fs"], + hooks, + }, + null, + 2, + )}\n`, + "utf8", + ); + + const result = await setupAgentProject(input); + + assert.deepEqual( + JSON.parse( + await readFile(path.join(input.projectRoot, ".codex", "hooks.json"), "utf8"), + ), + { hooks }, + ); + // With the two keys gone the file is today's output, so nothing conflicts. + assert.deepEqual(result.conflicts, []); +}); + test("fresh project becomes a valid Codex project root without user Git setup", async () => { const input = await fixture("codex-root"); const first = await setupAgentProject(input); diff --git a/desktop/codex-hook-compatibility.test.mjs b/desktop/codex-hook-compatibility.test.mjs index d87fc55..2030367 100644 --- a/desktop/codex-hook-compatibility.test.mjs +++ b/desktop/codex-hook-compatibility.test.mjs @@ -194,6 +194,8 @@ test("installed token-free Codex UserPromptSubmit launcher freezes and injects e "utf8", ), ); + // Codex loads no hooks from a file with a top-level key it does not know. + assert.deepEqual(Object.keys(hooks), ["hooks"]); const command = hooks.hooks.UserPromptSubmit[0].hooks[0].command; assert.equal( command,