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.
This commit is contained in:
lishixiang 2026-05-28 18:47:04 +08:00
parent 50715e3894
commit dd360261fc
3 changed files with 65 additions and 0 deletions

View file

@ -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 },

View file

@ -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 },

View file

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