Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
528bdb4
Add Bash PermissionRequest hooks
abhinav-oai Apr 10, 2026
99458d2
Include approval context in PermissionRequest hooks
abhinav-oai Apr 12, 2026
f6517fa
Fix permission request hook precedence and retry context
abhinav-oai Apr 12, 2026
5096cc3
Document permission request hook flow
abhinav-oai Apr 12, 2026
86282db
Clarify permission request decision comment
abhinav-oai Apr 12, 2026
1bf5222
Clean up permission request approval flow
abhinav-oai Apr 12, 2026
920307e
Add permission request hook coverage for exec approvals
abhinav-oai Apr 13, 2026
37e9f25
Run permission hooks for network approvals
abhinav-oai Apr 13, 2026
8e7a23c
Reshape permission request hook payload
abhinav-oai Apr 13, 2026
20e0ffa
Simplify permission request hook context
abhinav-oai Apr 13, 2026
2563661
Restore permission request attempt context
abhinav-oai Apr 13, 2026
04294e0
Trim PermissionRequest hook inputs
abhinav-oai Apr 13, 2026
75cc778
Use Bash for network approval hook payloads
abhinav-oai Apr 13, 2026
32e26c4
Simplify permission request hook plumbing
abhinav-oai Apr 13, 2026
144fcbe
Drop PermissionRequestApprovalAttempt
abhinav-oai Apr 14, 2026
7e48693
fix lint
abhinav-oai Apr 14, 2026
6cb8d62
Merge branch 'main' into codex/permission-request-hooks-base
abhinav-oai Apr 14, 2026
c8c8615
Merge branch 'main' into codex/permission-request-hooks-base
abhinav-oai Apr 15, 2026
236a11b
Merge origin/main into permission request hooks
abhinav-oai Apr 17, 2026
38e9c3d
Simplify approval hook helper
abhinav-oai Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/analytics/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ pub(crate) fn codex_hook_run_metadata(
fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
match event_name {
HookEventName::PreToolUse => "PreToolUse",
HookEventName::PermissionRequest => "PermissionRequest",
HookEventName::PostToolUse => "PostToolUse",
HookEventName::SessionStart => "SessionStart",
HookEventName::UserPromptSubmit => "UserPromptSubmit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"permissionRequest",
"postToolUse",
"sessionStart",
"userPromptSubmit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8484,6 +8484,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"permissionRequest",
"postToolUse",
"sessionStart",
"userPromptSubmit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5212,6 +5212,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"permissionRequest",
"postToolUse",
"sessionStart",
"userPromptSubmit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"permissionRequest",
"postToolUse",
"sessionStart",
"userPromptSubmit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"permissionRequest",
"postToolUse",
"sessionStart",
"userPromptSubmit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

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

export type HookEventName = "preToolUse" | "postToolUse" | "sessionStart" | "userPromptSubmit" | "stop";
export type HookEventName = "preToolUse" | "permissionRequest" | "postToolUse" | "sessionStart" | "userPromptSubmit" | "stop";
2 changes: 1 addition & 1 deletion codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ v2_enum_from_core!(

v2_enum_from_core!(
pub enum HookEventName from CoreHookEventName {
PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop
PreToolUse, PermissionRequest, PostToolUse, SessionStart, UserPromptSubmit, Stop
}
);

Expand Down
38 changes: 38 additions & 0 deletions codex-rs/core/src/hook_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use std::time::Duration;

use codex_analytics::HookRunFact;
use codex_analytics::build_track_events_context;
use codex_hooks::PermissionRequestDecision;
use codex_hooks::PermissionRequestOutcome;
use codex_hooks::PermissionRequestRequest;
use codex_hooks::PostToolUseOutcome;
use codex_hooks::PostToolUseRequest;
use codex_hooks::PreToolUseOutcome;
Expand Down Expand Up @@ -31,6 +34,7 @@ use serde_json::Value;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::event_mapping::parse_turn_item;
use crate::tools::sandboxing::PermissionRequestPayload;

pub(crate) struct HookRuntimeOutcome {
pub should_stop: bool,
Expand Down Expand Up @@ -153,6 +157,39 @@ pub(crate) async fn run_pre_tool_use_hooks(
if should_block { block_reason } else { None }
}

// PermissionRequest hooks share the same preview/start/completed event flow as
// other hook types, but they return an optional decision instead of mutating
// tool input or post-run state.
pub(crate) async fn run_permission_request_hooks(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
run_id_suffix: &str,
payload: PermissionRequestPayload,
) -> Option<PermissionRequestDecision> {
let request = PermissionRequestRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: hook_permission_mode(turn_context),
tool_name: payload.tool_name,
run_id_suffix: run_id_suffix.to_string(),
command: payload.command,
description: payload.description,
};
let preview_runs = sess.hooks().preview_permission_request(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;

let PermissionRequestOutcome {
hook_events,
decision,
} = sess.hooks().run_permission_request(request).await;
emit_hook_completed_events(sess, turn_context, hook_events).await;

decision
}

pub(crate) async fn run_post_tool_use_hooks(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
Expand Down Expand Up @@ -390,6 +427,7 @@ fn hook_run_analytics_payload(
fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str); 3] {
let hook_name = match run.event_name {
HookEventName::PreToolUse => "PreToolUse",
HookEventName::PermissionRequest => "PermissionRequest",
HookEventName::PostToolUse => "PostToolUse",
HookEventName::SessionStart => "SessionStart",
HookEventName::UserPromptSubmit => "UserPromptSubmit",
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/src/tools/handlers/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ fn shell_command_payload_command(payload: &ToolPayload) -> Option<String> {
struct RunExecLikeArgs {
tool_name: String,
exec_params: ExecParams,
hook_command: String,
additional_permissions: Option<PermissionProfile>,
prefix_rule: Option<Vec<String>>,
session: Arc<crate::codex::Session>,
Expand Down Expand Up @@ -241,6 +242,7 @@ impl ToolHandler for ShellHandler {
Self::run_exec_like(RunExecLikeArgs {
tool_name: tool_name.display(),
exec_params,
hook_command: codex_shell_command::parse_command::shlex_join(&params.command),
additional_permissions: params.additional_permissions.clone(),
prefix_rule,
session,
Expand All @@ -258,6 +260,7 @@ impl ToolHandler for ShellHandler {
Self::run_exec_like(RunExecLikeArgs {
tool_name: tool_name.display(),
exec_params,
hook_command: codex_shell_command::parse_command::shlex_join(&params.command),
additional_permissions: None,
prefix_rule: None,
session,
Expand Down Expand Up @@ -366,6 +369,7 @@ impl ToolHandler for ShellCommandHandler {
ShellHandler::run_exec_like(RunExecLikeArgs {
tool_name: tool_name.display(),
exec_params,
hook_command: params.command,
additional_permissions: params.additional_permissions.clone(),
prefix_rule,
session,
Expand All @@ -384,6 +388,7 @@ impl ShellHandler {
let RunExecLikeArgs {
tool_name,
exec_params,
hook_command,
additional_permissions,
prefix_rule,
session,
Expand Down Expand Up @@ -514,6 +519,7 @@ impl ShellHandler {

let req = ShellRequest {
command: exec_params.command.clone(),
hook_command,
cwd: exec_params.cwd.clone(),
timeout_ms: exec_params.expiration.timeout_ms(),
env: exec_params.env.clone(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/handlers/unified_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ impl ToolHandler for UnifiedExecHandler {
.exec_command(
ExecCommandRequest {
command,
hook_command: args.cmd,
process_id,
yield_time_ms,
max_output_tokens,
Expand Down
54 changes: 49 additions & 5 deletions codex-rs/core/src/tools/network_approval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use crate::guardian::guardian_timeout_message;
use crate::guardian::new_guardian_review_id;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::hook_runtime::run_permission_request_hooks;
use crate::network_policy_decision::denied_network_policy_message;
use crate::tools::sandboxing::PermissionRequestPayload;
use crate::tools::sandboxing::ToolError;
use codex_hooks::PermissionRequestDecision;
use codex_network_proxy::BlockedRequest;
use codex_network_proxy::BlockedRequestObserver;
use codex_network_proxy::NetworkDecision;
Expand Down Expand Up @@ -43,6 +46,7 @@ pub(crate) enum NetworkApprovalMode {
pub(crate) struct NetworkApprovalSpec {
pub network: Option<NetworkProxy>,
pub mode: NetworkApprovalMode,
pub command: String,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -172,6 +176,7 @@ impl PendingHostApproval {
struct ActiveNetworkApprovalCall {
registration_id: String,
turn_id: String,
command: String,
}

pub(crate) struct NetworkApprovalService {
Expand Down Expand Up @@ -204,14 +209,15 @@ impl NetworkApprovalService {
other_approved_hosts.extend(approved_hosts.iter().cloned());
}

async fn register_call(&self, registration_id: String, turn_id: String) {
async fn register_call(&self, registration_id: String, turn_id: String, command: String) {
let mut active_calls = self.active_calls.lock().await;
let key = registration_id.clone();
active_calls.insert(
key,
Arc::new(ActiveNetworkApprovalCall {
registration_id,
turn_id,
command,
}),
);
}
Expand Down Expand Up @@ -371,6 +377,46 @@ impl NetworkApprovalService {
};
let owner_call = self.resolve_single_active_call().await;
let guardian_approval_id = Self::approval_id_for_key(&key);
let prompt_command = vec!["network-access".to_string(), target.clone()];
let command = owner_call
.as_ref()
.map_or_else(|| prompt_command.join(" "), |call| call.command.clone());
if let Some(permission_request_decision) = run_permission_request_hooks(
&session,
&turn_context,
&guardian_approval_id,
PermissionRequestPayload {
tool_name: "Bash".to_string(),
command,
description: Some(format!("network-access {target}")),
},
)
.await
{
match permission_request_decision {
PermissionRequestDecision::Allow => {
pending
.set_decision(PendingApprovalDecision::AllowOnce)
.await;
let mut pending_approvals = self.pending_host_approvals.lock().await;
pending_approvals.remove(&key);
return NetworkDecision::Allow;
}
PermissionRequestDecision::Deny { message } => {
if let Some(owner_call) = owner_call.as_ref() {
self.record_call_outcome(
&owner_call.registration_id,
NetworkApprovalOutcome::DeniedByPolicy(message),
)
.await;
}
pending.set_decision(PendingApprovalDecision::Deny).await;
let mut pending_approvals = self.pending_host_approvals.lock().await;
pending_approvals.remove(&key);
return NetworkDecision::deny(REASON_NOT_ALLOWED);
}
}
}
let use_guardian = routes_approval_to_guardian(&turn_context);
let guardian_review_id = use_guardian.then(new_guardian_review_id);
let approval_decision = if let Some(review_id) = guardian_review_id.clone() {
Expand All @@ -392,13 +438,11 @@ impl NetworkApprovalService {
)
.await
} else {
let approval_id = Self::approval_id_for_key(&key);
let prompt_command = vec!["network-access".to_string(), target.clone()];
let available_decisions = None;
session
.request_command_approval(
turn_context.as_ref(),
approval_id,
guardian_approval_id,
/*approval_id*/ None,
prompt_command,
turn_context.cwd.clone(),
Expand Down Expand Up @@ -590,7 +634,7 @@ pub(crate) async fn begin_network_approval(
session
.services
.network_approval
.register_call(registration_id.clone(), turn_id.to_string())
.register_call(registration_id.clone(), turn_id.to_string(), spec.command)
.await;

Some(ActiveNetworkApproval {
Expand Down
24 changes: 20 additions & 4 deletions codex-rs/core/src/tools/network_approval_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,11 @@ fn denied_blocked_request(host: &str) -> BlockedRequest {
async fn record_blocked_request_sets_policy_outcome_for_owner_call() {
let service = NetworkApprovalService::default();
service
.register_call("registration-1".to_string(), "turn-1".to_string())
.register_call(
"registration-1".to_string(),
"turn-1".to_string(),
"curl http://example.com".to_string(),
)
.await;

service
Expand All @@ -230,7 +234,11 @@ async fn record_blocked_request_sets_policy_outcome_for_owner_call() {
async fn blocked_request_policy_does_not_override_user_denial_outcome() {
let service = NetworkApprovalService::default();
service
.register_call("registration-1".to_string(), "turn-1".to_string())
.register_call(
"registration-1".to_string(),
"turn-1".to_string(),
"curl http://example.com".to_string(),
)
.await;

service
Expand All @@ -250,10 +258,18 @@ async fn blocked_request_policy_does_not_override_user_denial_outcome() {
async fn record_blocked_request_ignores_ambiguous_unattributed_blocked_requests() {
let service = NetworkApprovalService::default();
service
.register_call("registration-1".to_string(), "turn-1".to_string())
.register_call(
"registration-1".to_string(),
"turn-1".to_string(),
"curl http://example.com".to_string(),
)
.await;
service
.register_call("registration-2".to_string(), "turn-1".to_string())
.register_call(
"registration-2".to_string(),
"turn-1".to_string(),
"gh api /foo".to_string(),
)
.await;

service
Expand Down
Loading
Loading