mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
## Why We need `PermissionRequest` hook support! Also addresses: - https://github.com/openai/codex/issues/16301 - run a script on Hook to do things like play a sound to draw attention but actually no-op so user can still approve - can omit the `decision` object from output or just have the script exit 0 and print nothing - https://github.com/openai/codex/issues/15311 - let the script approve/deny on its own - external UI what will run on Hook and relay decision back to codex ## Reviewer Note There's a lot of plumbing for the new hook, key files to review are: - New hook added in `codex-rs/hooks/src/events/permission_request.rs` - Wiring for network approvals `codex-rs/core/src/tools/network_approval.rs` - Wiring for tool orchestrator `codex-rs/core/src/tools/orchestrator.rs` - Wiring for execve `codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs` ## What - Wires shell, unified exec, and network approval prompts into the `PermissionRequest` hook flow. - Lets hooks allow or deny approval prompts; quiet or invalid hooks fall back to the normal approval path. - Uses `tool_input.description` for user-facing context when it helps: - shell / `exec_command`: the request justification, when present - network approvals: `network-access <domain>` - Uses `tool_name: Bash` for shell, unified exec, and network approval permission-request hooks. - For network approvals, passes the originating command in `tool_input.command` when there is a single owning call; otherwise falls back to the synthetic `network-access ...` command. <details> <summary>Example `PermissionRequest` hook input for a shell approval</summary> ```json { "session_id": "<session-id>", "turn_id": "<turn-id>", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/path/to/cwd", "hook_event_name": "PermissionRequest", "model": "gpt-5", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "rm -f /tmp/example" } } ``` </details> <details> <summary>Example `PermissionRequest` hook input for an escalated `exec_command` request</summary> ```json { "session_id": "<session-id>", "turn_id": "<turn-id>", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/path/to/cwd", "hook_event_name": "PermissionRequest", "model": "gpt-5", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "cp /tmp/source.json /Users/alice/export/source.json", "description": "Need to copy a generated file outside the workspace" } } ``` </details> <details> <summary>Example `PermissionRequest` hook input for a network approval</summary> ```json { "session_id": "<session-id>", "turn_id": "<turn-id>", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/path/to/cwd", "hook_event_name": "PermissionRequest", "model": "gpt-5", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "curl http://codex-network-test.invalid", "description": "network-access http://codex-network-test.invalid" } } ``` </details> ## Follow-ups - Implement the `PermissionRequest` semantics for `updatedInput`, `updatedPermissions`, `interrupt`, and suggestions / `permission_suggestions` - Add `PermissionRequest` support for the `request_permissions` tool path --------- Co-authored-by: Codex <noreply@openai.com>
180 lines
5.3 KiB
Rust
180 lines
5.3 KiB
Rust
use codex_config::ConfigLayerStack;
|
|
use tokio::process::Command;
|
|
|
|
use crate::engine::ClaudeHooksEngine;
|
|
use crate::engine::CommandShell;
|
|
use crate::events::permission_request::PermissionRequestOutcome;
|
|
use crate::events::permission_request::PermissionRequestRequest;
|
|
use crate::events::post_tool_use::PostToolUseOutcome;
|
|
use crate::events::post_tool_use::PostToolUseRequest;
|
|
use crate::events::pre_tool_use::PreToolUseOutcome;
|
|
use crate::events::pre_tool_use::PreToolUseRequest;
|
|
use crate::events::session_start::SessionStartOutcome;
|
|
use crate::events::session_start::SessionStartRequest;
|
|
use crate::events::stop::StopOutcome;
|
|
use crate::events::stop::StopRequest;
|
|
use crate::events::user_prompt_submit::UserPromptSubmitOutcome;
|
|
use crate::events::user_prompt_submit::UserPromptSubmitRequest;
|
|
use crate::types::Hook;
|
|
use crate::types::HookEvent;
|
|
use crate::types::HookPayload;
|
|
use crate::types::HookResponse;
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct HooksConfig {
|
|
pub legacy_notify_argv: Option<Vec<String>>,
|
|
pub feature_enabled: bool,
|
|
pub config_layer_stack: Option<ConfigLayerStack>,
|
|
pub shell_program: Option<String>,
|
|
pub shell_args: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Hooks {
|
|
after_agent: Vec<Hook>,
|
|
after_tool_use: Vec<Hook>,
|
|
engine: ClaudeHooksEngine,
|
|
}
|
|
|
|
impl Default for Hooks {
|
|
fn default() -> Self {
|
|
Self::new(HooksConfig::default())
|
|
}
|
|
}
|
|
|
|
impl Hooks {
|
|
pub fn new(config: HooksConfig) -> Self {
|
|
let after_agent = config
|
|
.legacy_notify_argv
|
|
.filter(|argv| !argv.is_empty() && !argv[0].is_empty())
|
|
.map(crate::notify_hook)
|
|
.into_iter()
|
|
.collect();
|
|
let engine = ClaudeHooksEngine::new(
|
|
config.feature_enabled,
|
|
config.config_layer_stack.as_ref(),
|
|
CommandShell {
|
|
program: config.shell_program.unwrap_or_default(),
|
|
args: config.shell_args,
|
|
},
|
|
);
|
|
Self {
|
|
after_agent,
|
|
after_tool_use: Vec::new(),
|
|
engine,
|
|
}
|
|
}
|
|
|
|
pub fn startup_warnings(&self) -> &[String] {
|
|
self.engine.warnings()
|
|
}
|
|
|
|
fn hooks_for_event(&self, hook_event: &HookEvent) -> &[Hook] {
|
|
match hook_event {
|
|
HookEvent::AfterAgent { .. } => &self.after_agent,
|
|
HookEvent::AfterToolUse { .. } => &self.after_tool_use,
|
|
}
|
|
}
|
|
|
|
pub async fn dispatch(&self, hook_payload: HookPayload) -> Vec<HookResponse> {
|
|
let hooks = self.hooks_for_event(&hook_payload.hook_event);
|
|
let mut outcomes = Vec::with_capacity(hooks.len());
|
|
for hook in hooks {
|
|
let outcome = hook.execute(&hook_payload).await;
|
|
let should_abort_operation = outcome.result.should_abort_operation();
|
|
outcomes.push(outcome);
|
|
if should_abort_operation {
|
|
break;
|
|
}
|
|
}
|
|
|
|
outcomes
|
|
}
|
|
|
|
pub fn preview_session_start(
|
|
&self,
|
|
request: &SessionStartRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_session_start(request)
|
|
}
|
|
|
|
pub fn preview_pre_tool_use(
|
|
&self,
|
|
request: &PreToolUseRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_pre_tool_use(request)
|
|
}
|
|
|
|
pub fn preview_permission_request(
|
|
&self,
|
|
request: &PermissionRequestRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_permission_request(request)
|
|
}
|
|
|
|
pub fn preview_post_tool_use(
|
|
&self,
|
|
request: &PostToolUseRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_post_tool_use(request)
|
|
}
|
|
|
|
pub async fn run_session_start(
|
|
&self,
|
|
request: SessionStartRequest,
|
|
turn_id: Option<String>,
|
|
) -> SessionStartOutcome {
|
|
self.engine.run_session_start(request, turn_id).await
|
|
}
|
|
|
|
pub async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome {
|
|
self.engine.run_pre_tool_use(request).await
|
|
}
|
|
|
|
pub async fn run_permission_request(
|
|
&self,
|
|
request: PermissionRequestRequest,
|
|
) -> PermissionRequestOutcome {
|
|
self.engine.run_permission_request(request).await
|
|
}
|
|
|
|
pub async fn run_post_tool_use(&self, request: PostToolUseRequest) -> PostToolUseOutcome {
|
|
self.engine.run_post_tool_use(request).await
|
|
}
|
|
|
|
pub fn preview_user_prompt_submit(
|
|
&self,
|
|
request: &UserPromptSubmitRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_user_prompt_submit(request)
|
|
}
|
|
|
|
pub async fn run_user_prompt_submit(
|
|
&self,
|
|
request: UserPromptSubmitRequest,
|
|
) -> UserPromptSubmitOutcome {
|
|
self.engine.run_user_prompt_submit(request).await
|
|
}
|
|
|
|
pub fn preview_stop(
|
|
&self,
|
|
request: &StopRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_stop(request)
|
|
}
|
|
|
|
pub async fn run_stop(&self, request: StopRequest) -> StopOutcome {
|
|
self.engine.run_stop(request).await
|
|
}
|
|
}
|
|
|
|
pub fn command_from_argv(argv: &[String]) -> Option<Command> {
|
|
let (program, args) = argv.split_first()?;
|
|
if program.is_empty() {
|
|
return None;
|
|
}
|
|
let mut command = Command::new(program);
|
|
command.args(args);
|
|
Some(command)
|
|
}
|