Skip to main content

coding_agent_hooks/agents/
codex.rs

1//! OpenAI Codex CLI hook protocol implementation.
2//!
3//! Codex uses Proceed/Block/Modify decisions instead of allow/deny.
4
5use anyhow::Result;
6use serde_json::Value;
7
8use super::{AgentKind, resolve_tool_name};
9use crate::input::ToolUseHookInput;
10use crate::protocol::{HookProtocol, json_str, json_str_any, json_value_any};
11
12pub struct CodexProtocol;
13
14impl HookProtocol for CodexProtocol {
15    fn agent(&self) -> AgentKind {
16        AgentKind::Codex
17    }
18
19    fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
20        let tool_name = json_str(raw, "tool_name").to_string();
21        let original = tool_name.clone();
22        let resolved = resolve_tool_name(AgentKind::Codex, &tool_name).to_string();
23
24        Ok(ToolUseHookInput {
25            session_id: json_str(raw, "session_id").to_string(),
26            transcript_path: json_str(raw, "transcript_path").to_string(),
27            cwd: json_str_any(raw, &["cwd", "working_directory"]).to_string(),
28            permission_mode: "default".to_string(),
29            hook_event_name: json_str(raw, "hook_event_name").to_string(),
30            tool_name: resolved,
31            tool_input: json_value_any(raw, &["tool_input", "input"])
32                .unwrap_or(Value::Object(serde_json::Map::new())),
33            tool_use_id: None,
34            tool_response: raw.get("tool_response").cloned(),
35            agent: Some(AgentKind::Codex),
36            original_tool_name: Some(original),
37        })
38    }
39
40    // Codex uses "proceed"/"block"/"modify" instead of "allow"/"deny"
41    fn format_allow(
42        &self,
43        reason: Option<&str>,
44        _context: Option<&str>,
45        updated_input: Option<Value>,
46    ) -> Value {
47        let mut output = serde_json::json!({ "decision": "proceed" });
48        if let Some(r) = reason {
49            output["reason"] = Value::String(r.to_string());
50        }
51        if let Some(ui) = updated_input {
52            output["decision"] = Value::String("modify".to_string());
53            output["tool_input"] = ui;
54        }
55        output
56    }
57
58    fn format_deny(&self, reason: &str, _context: Option<&str>) -> Value {
59        serde_json::json!({
60            "decision": "block",
61            "message": reason
62        })
63    }
64
65    fn format_ask(&self, _reason: Option<&str>, _context: Option<&str>) -> Value {
66        serde_json::json!({ "decision": "proceed" })
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn parse_codex_shell() {
76        let raw = serde_json::json!({
77            "session_id": "codex-123",
78            "cwd": "/home/user",
79            "hook_event_name": "PreToolUse",
80            "tool_name": "shell",
81            "tool_input": {"command": "git status"}
82        });
83        let input = CodexProtocol.parse_tool_use(&raw).unwrap();
84        assert_eq!(input.tool_name, "Bash");
85        assert_eq!(input.original_tool_name.as_deref(), Some("shell"));
86    }
87
88    #[test]
89    fn format_allow_codex() {
90        assert_eq!(
91            CodexProtocol.format_allow(None, None, None)["decision"],
92            "proceed"
93        );
94    }
95
96    #[test]
97    fn format_deny_codex() {
98        assert_eq!(CodexProtocol.format_deny("no", None)["decision"], "block");
99    }
100
101    #[test]
102    fn format_modify_codex() {
103        let ui = serde_json::json!({"command": "sandboxed"});
104        let out = CodexProtocol.format_allow(None, None, Some(ui));
105        assert_eq!(out["decision"], "modify");
106    }
107}