Skip to content

Commit 3667eae

Browse files
Altruistusclaude
andauthored
CM-73962: Stop sending explicit allow decisions from the Codex hook (#566)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 338f081 commit 3667eae

3 files changed

Lines changed: 65 additions & 13 deletions

File tree

‎cycode/cli/apps/ai_guardrails/ides/codex.py‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -269,21 +269,18 @@ def parse_hook_payload(self, raw_payload: dict) -> AIHookPayload:
269269
)
270270

271271
def build_hook_response(self, decision: HookDecision) -> dict:
272-
# Codex accepts the same hook response shapes as Claude Code:
273-
# - PROMPT: empty for allow, {"decision": "block", "reason": ...} for deny
274-
# - PreToolUse: hookSpecificOutput.permissionDecision
272+
# Codex hook response shapes:
273+
# - allow: empty (no objection). Older Codex runtimes reject an explicit
274+
# PreToolUse permissionDecision "allow" as unsupported and report a
275+
# failed hook on every call, so never send it.
276+
# - PROMPT deny: {"decision": "block", "reason": ...}
277+
# - PreToolUse deny: hookSpecificOutput.permissionDecision
278+
if decision.action == DecisionAction.ALLOW:
279+
return {}
280+
275281
if decision.event_type == AiHookEventType.PROMPT:
276-
if decision.action == DecisionAction.ALLOW:
277-
return {}
278282
return {'decision': 'block', 'reason': decision.user_message or ''}
279283

280-
if decision.action == DecisionAction.ALLOW:
281-
return {
282-
'hookSpecificOutput': {
283-
'hookEventName': 'PreToolUse',
284-
'permissionDecision': 'allow',
285-
}
286-
}
287284
return {
288285
'hookSpecificOutput': {
289286
'hookEventName': 'PreToolUse',

‎tests/cli/commands/ai_guardrails/ides/test_codex.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ def test_build_prompt_responses() -> None:
9797
def test_build_mcp_execution_allow_and_deny() -> None:
9898
codex = Codex()
9999
allow = codex.build_hook_response(HookDecision.allow(AiHookEventType.MCP_EXECUTION))
100-
assert allow == {'hookSpecificOutput': {'hookEventName': 'PreToolUse', 'permissionDecision': 'allow'}}
100+
# Explicit permissionDecision "allow" is rejected by older Codex runtimes.
101+
assert allow == {}
101102

102103
deny = codex.build_hook_response(HookDecision.deny(AiHookEventType.MCP_EXECUTION, 'secret in args!'))
103104
assert deny == {

‎tests/cli/commands/ai_guardrails/scan/test_scan_command.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,60 @@ def test_copilot_cli_payload_skipped(
265265
assert json.loads(capsys.readouterr().out) == {}
266266

267267

268+
class TestCodexMcpResponses:
269+
"""Codex PreToolUse responses for MCP calls through scan_command."""
270+
271+
_PAYLOAD = { # noqa: RUF012
272+
'hook_event_name': 'PreToolUse',
273+
'session_id': 'session-123',
274+
'turn_id': 'turn-1',
275+
'tool_name': 'mcp__grafana__query_loki_logs',
276+
'tool_input': {'logql': '{app="api"}'},
277+
}
278+
279+
def test_permitted_mcp_call_returns_empty_response(
280+
self,
281+
mock_ctx: MagicMock,
282+
mocker: MockerFixture,
283+
capsys: pytest.CaptureFixture[str],
284+
mock_scan_command_deps: dict[str, MagicMock],
285+
) -> None:
286+
"""Older Codex runtimes reject permissionDecision "allow" as unsupported
287+
and report a failed hook on every MCP call - allow must be empty."""
288+
mocker.patch('sys.stdin', StringIO(json.dumps(self._PAYLOAD)))
289+
mock_scan_command_deps['load_policy'].return_value = {'fail_open': True}
290+
mock_scan_command_deps['get_handler'].return_value = MagicMock(
291+
return_value=HookDecision.allow(AiHookEventType.MCP_EXECUTION)
292+
)
293+
294+
scan_command(mock_ctx, ide='codex')
295+
296+
assert json.loads(capsys.readouterr().out) == {}
297+
298+
def test_blocked_mcp_call_returns_deny_with_reason(
299+
self,
300+
mock_ctx: MagicMock,
301+
mocker: MockerFixture,
302+
capsys: pytest.CaptureFixture[str],
303+
mock_scan_command_deps: dict[str, MagicMock],
304+
) -> None:
305+
mocker.patch('sys.stdin', StringIO(json.dumps(self._PAYLOAD)))
306+
mock_scan_command_deps['load_policy'].return_value = {'fail_open': True}
307+
mock_scan_command_deps['get_handler'].return_value = MagicMock(
308+
return_value=HookDecision.deny(AiHookEventType.MCP_EXECUTION, 'Secret detected in MCP arguments')
309+
)
310+
311+
scan_command(mock_ctx, ide='codex')
312+
313+
assert json.loads(capsys.readouterr().out) == {
314+
'hookSpecificOutput': {
315+
'hookEventName': 'PreToolUse',
316+
'permissionDecision': 'deny',
317+
'permissionDecisionReason': 'Secret detected in MCP arguments',
318+
}
319+
}
320+
321+
268322
class TestSelfDetach:
269323
"""Report-mode scans respawn detached and release the IDE immediately."""
270324

0 commit comments

Comments
 (0)