Skip to content

Commit 8494e5b

Browse files
abhinav-oaicodex
andauthored
Add PermissionRequest hooks support (#17563)
## Why We need `PermissionRequest` hook support! Also addresses: - #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 - #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>
1 parent d0047de commit 8494e5b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1821
-36
lines changed

codex-rs/analytics/src/events.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ pub(crate) fn codex_hook_run_metadata(
567567
fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
568568
match event_name {
569569
HookEventName::PreToolUse => "PreToolUse",
570+
HookEventName::PermissionRequest => "PermissionRequest",
570571
HookEventName::PostToolUse => "PostToolUse",
571572
HookEventName::SessionStart => "SessionStart",
572573
HookEventName::UserPromptSubmit => "UserPromptSubmit",

codex-rs/app-server-protocol/schema/json/ServerNotification.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,7 @@
14071407
"HookEventName": {
14081408
"enum": [
14091409
"preToolUse",
1410+
"permissionRequest",
14101411
"postToolUse",
14111412
"sessionStart",
14121413
"userPromptSubmit",

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8509,6 +8509,7 @@
85098509
"HookEventName": {
85108510
"enum": [
85118511
"preToolUse",
8512+
"permissionRequest",
85128513
"postToolUse",
85138514
"sessionStart",
85148515
"userPromptSubmit",

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5217,6 +5217,7 @@
52175217
"HookEventName": {
52185218
"enum": [
52195219
"preToolUse",
5220+
"permissionRequest",
52205221
"postToolUse",
52215222
"sessionStart",
52225223
"userPromptSubmit",

codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"HookEventName": {
99
"enum": [
1010
"preToolUse",
11+
"permissionRequest",
1112
"postToolUse",
1213
"sessionStart",
1314
"userPromptSubmit",

codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"HookEventName": {
99
"enum": [
1010
"preToolUse",
11+
"permissionRequest",
1112
"postToolUse",
1213
"sessionStart",
1314
"userPromptSubmit",

codex-rs/app-server-protocol/schema/typescript/v2/HookEventName.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
44

5-
export type HookEventName = "preToolUse" | "postToolUse" | "sessionStart" | "userPromptSubmit" | "stop";
5+
export type HookEventName = "preToolUse" | "permissionRequest" | "postToolUse" | "sessionStart" | "userPromptSubmit" | "stop";

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ v2_enum_from_core!(
381381

382382
v2_enum_from_core!(
383383
pub enum HookEventName from CoreHookEventName {
384-
PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop
384+
PreToolUse, PermissionRequest, PostToolUse, SessionStart, UserPromptSubmit, Stop
385385
}
386386
);
387387

codex-rs/core/src/hook_runtime.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use std::time::Duration;
44

55
use codex_analytics::HookRunFact;
66
use codex_analytics::build_track_events_context;
7+
use codex_hooks::PermissionRequestDecision;
8+
use codex_hooks::PermissionRequestOutcome;
9+
use codex_hooks::PermissionRequestRequest;
710
use codex_hooks::PostToolUseOutcome;
811
use codex_hooks::PostToolUseRequest;
912
use codex_hooks::PreToolUseOutcome;
@@ -31,6 +34,7 @@ use serde_json::Value;
3134
use crate::codex::Session;
3235
use crate::codex::TurnContext;
3336
use crate::event_mapping::parse_turn_item;
37+
use crate::tools::sandboxing::PermissionRequestPayload;
3438

3539
pub(crate) struct HookRuntimeOutcome {
3640
pub should_stop: bool,
@@ -153,6 +157,39 @@ pub(crate) async fn run_pre_tool_use_hooks(
153157
if should_block { block_reason } else { None }
154158
}
155159

160+
// PermissionRequest hooks share the same preview/start/completed event flow as
161+
// other hook types, but they return an optional decision instead of mutating
162+
// tool input or post-run state.
163+
pub(crate) async fn run_permission_request_hooks(
164+
sess: &Arc<Session>,
165+
turn_context: &Arc<TurnContext>,
166+
run_id_suffix: &str,
167+
payload: PermissionRequestPayload,
168+
) -> Option<PermissionRequestDecision> {
169+
let request = PermissionRequestRequest {
170+
session_id: sess.conversation_id,
171+
turn_id: turn_context.sub_id.clone(),
172+
cwd: turn_context.cwd.to_path_buf(),
173+
transcript_path: sess.hook_transcript_path().await,
174+
model: turn_context.model_info.slug.clone(),
175+
permission_mode: hook_permission_mode(turn_context),
176+
tool_name: payload.tool_name,
177+
run_id_suffix: run_id_suffix.to_string(),
178+
command: payload.command,
179+
description: payload.description,
180+
};
181+
let preview_runs = sess.hooks().preview_permission_request(&request);
182+
emit_hook_started_events(sess, turn_context, preview_runs).await;
183+
184+
let PermissionRequestOutcome {
185+
hook_events,
186+
decision,
187+
} = sess.hooks().run_permission_request(request).await;
188+
emit_hook_completed_events(sess, turn_context, hook_events).await;
189+
190+
decision
191+
}
192+
156193
pub(crate) async fn run_post_tool_use_hooks(
157194
sess: &Arc<Session>,
158195
turn_context: &Arc<TurnContext>,
@@ -390,6 +427,7 @@ fn hook_run_analytics_payload(
390427
fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str); 3] {
391428
let hook_name = match run.event_name {
392429
HookEventName::PreToolUse => "PreToolUse",
430+
HookEventName::PermissionRequest => "PermissionRequest",
393431
HookEventName::PostToolUse => "PostToolUse",
394432
HookEventName::SessionStart => "SessionStart",
395433
HookEventName::UserPromptSubmit => "UserPromptSubmit",

codex-rs/core/src/tools/handlers/shell.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ fn shell_command_payload_command(payload: &ToolPayload) -> Option<String> {
7777
struct RunExecLikeArgs {
7878
tool_name: String,
7979
exec_params: ExecParams,
80+
hook_command: String,
8081
additional_permissions: Option<PermissionProfile>,
8182
prefix_rule: Option<Vec<String>>,
8283
session: Arc<crate::codex::Session>,
@@ -241,6 +242,7 @@ impl ToolHandler for ShellHandler {
241242
Self::run_exec_like(RunExecLikeArgs {
242243
tool_name: tool_name.display(),
243244
exec_params,
245+
hook_command: codex_shell_command::parse_command::shlex_join(&params.command),
244246
additional_permissions: params.additional_permissions.clone(),
245247
prefix_rule,
246248
session,
@@ -258,6 +260,7 @@ impl ToolHandler for ShellHandler {
258260
Self::run_exec_like(RunExecLikeArgs {
259261
tool_name: tool_name.display(),
260262
exec_params,
263+
hook_command: codex_shell_command::parse_command::shlex_join(&params.command),
261264
additional_permissions: None,
262265
prefix_rule: None,
263266
session,
@@ -366,6 +369,7 @@ impl ToolHandler for ShellCommandHandler {
366369
ShellHandler::run_exec_like(RunExecLikeArgs {
367370
tool_name: tool_name.display(),
368371
exec_params,
372+
hook_command: params.command,
369373
additional_permissions: params.additional_permissions.clone(),
370374
prefix_rule,
371375
session,
@@ -384,6 +388,7 @@ impl ShellHandler {
384388
let RunExecLikeArgs {
385389
tool_name,
386390
exec_params,
391+
hook_command,
387392
additional_permissions,
388393
prefix_rule,
389394
session,
@@ -514,6 +519,7 @@ impl ShellHandler {
514519

515520
let req = ShellRequest {
516521
command: exec_params.command.clone(),
522+
hook_command,
517523
cwd: exec_params.cwd.clone(),
518524
timeout_ms: exec_params.expiration.timeout_ms(),
519525
env: exec_params.env.clone(),

0 commit comments

Comments
 (0)