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(); + }); } });