From dd360261fc5a0a6df7f212c9a373fdc4bd28dced Mon Sep 17 00:00:00 2001 From: lishixiang Date: Thu, 28 May 2026 18:47:04 +0800 Subject: [PATCH] fix(hooks): emit top-level additionalContext under Codex runtime The PostToolUse staleness hook always emitted Claude Code's `hookSpecificOutput.additionalContext` shape. Under Codex, the SDK expects a top-level `additionalContext` field, so Codex rejected the hook output with "invalid post-tool-use JSON output". Add isCodexRuntime() (detects CODEX_THREAD_ID / CODEX_CI / CODEX_MANAGED_PACKAGE_ROOT) and branch sendHookResponse() to emit the SDK-standard top-level shape under Codex while keeping the Claude Code shape everywhere else. Applied to both the CJS hook (npm package) and the claude-plugin hook. Adds a regression test asserting Codex runtime produces top-level additionalContext with no hookSpecificOutput wrapper. --- gitnexus-claude-plugin/hooks/gitnexus-hook.js | 20 +++++++++++++++ gitnexus/hooks/claude/gitnexus-hook.cjs | 20 +++++++++++++++ gitnexus/test/unit/hooks.test.ts | 25 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/gitnexus-claude-plugin/hooks/gitnexus-hook.js b/gitnexus-claude-plugin/hooks/gitnexus-hook.js index 91c20c9d2..db1b366fe 100644 --- a/gitnexus-claude-plugin/hooks/gitnexus-hook.js +++ b/gitnexus-claude-plugin/hooks/gitnexus-hook.js @@ -238,10 +238,30 @@ function runGitNexusCli(args, cwd, timeout) { }); } +/** + * Detect whether the hook is running under Codex rather than Claude Code. + * Codex sets CODEX_* environment variables in its hook runtime. + */ +function isCodexRuntime() { + return Boolean( + process.env.CODEX_THREAD_ID || + process.env.CODEX_CI || + process.env.CODEX_MANAGED_PACKAGE_ROOT, + ); +} + /** * Emit a hook response with additional context for the agent. + * + * Claude Code expects the context nested under its hook-specific output + * object; Codex's SDK expects a top-level `additionalContext`. Emitting the + * wrong shape makes Codex reject the hook with "invalid post-tool-use JSON". */ function sendHookResponse(hookEventName, message) { + if (isCodexRuntime()) { + console.log(JSON.stringify({ additionalContext: message })); + return; + } console.log( JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: message }, diff --git a/gitnexus/hooks/claude/gitnexus-hook.cjs b/gitnexus/hooks/claude/gitnexus-hook.cjs index 9793bd7bc..17741da00 100755 --- a/gitnexus/hooks/claude/gitnexus-hook.cjs +++ b/gitnexus/hooks/claude/gitnexus-hook.cjs @@ -274,10 +274,30 @@ function handlePreToolUse(input) { } } +/** + * Detect whether the hook is running under Codex rather than Claude Code. + * Codex sets CODEX_* environment variables in its hook runtime. + */ +function isCodexRuntime() { + return Boolean( + process.env.CODEX_THREAD_ID || + process.env.CODEX_CI || + process.env.CODEX_MANAGED_PACKAGE_ROOT, + ); +} + /** * Emit a PostToolUse hook response with additional context for the agent. + * + * Claude Code expects the context nested under its hook-specific output + * object; Codex's SDK expects a top-level `additionalContext`. Emitting the + * wrong shape makes Codex reject the hook with "invalid post-tool-use JSON". */ function sendHookResponse(hookEventName, message) { + if (isCodexRuntime()) { + console.log(JSON.stringify({ additionalContext: message })); + return; + } console.log( JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: message }, diff --git a/gitnexus/test/unit/hooks.test.ts b/gitnexus/test/unit/hooks.test.ts index 892a19483..794f065fb 100644 --- a/gitnexus/test/unit/hooks.test.ts +++ b/gitnexus/test/unit/hooks.test.ts @@ -1438,6 +1438,31 @@ describe('PostToolUse staleness detection (integration)', () => { const output = parseHookOutput(result.stdout); expect(output).not.toBeNull(); }); + + it(`${label}: emits top-level additionalContext under Codex runtime`, () => { + fs.writeFileSync( + path.join(gitNexusDir, 'meta.json'), + JSON.stringify({ lastCommit: 'aaaaaaa0000000000000000000000000deadbeef', stats: {} }), + ); + + const result = runHook( + hookPath, + { + hook_event_name: 'PostToolUse', + tool_name: 'Bash', + tool_input: { command: 'git commit -m "test"' }, + tool_output: { exit_code: 0 }, + cwd: tmpDir, + }, + undefined, + { env: { ...process.env, CODEX_THREAD_ID: 'test-thread' } }, + ); + + // Codex expects top-level additionalContext, NOT hookSpecificOutput. + const parsed = JSON.parse(result.stdout.trim()); + expect(parsed.additionalContext).toContain('stale'); + expect(parsed.hookSpecificOutput).toBeUndefined(); + }); } });