Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"postToolUse",
"sessionStart",
"userPromptSubmit",
"stop"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7995,6 +7995,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"postToolUse",
"sessionStart",
"userPromptSubmit",
"stop"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4699,6 +4699,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"postToolUse",
"sessionStart",
"userPromptSubmit",
"stop"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"postToolUse",
"sessionStart",
"userPromptSubmit",
"stop"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"HookEventName": {
"enum": [
"preToolUse",
"postToolUse",
"sessionStart",
"userPromptSubmit",
"stop"
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" | "sessionStart" | "userPromptSubmit" | "stop";
export type HookEventName = "preToolUse" | "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 @@ -377,7 +377,7 @@ v2_enum_from_core!(

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

Expand Down
36 changes: 33 additions & 3 deletions codex-rs/core/src/hook_runtime.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::future::Future;
use std::sync::Arc;

use codex_hooks::PostToolUseOutcome;
use codex_hooks::PostToolUseRequest;
use codex_hooks::PreToolUseOutcome;
use codex_hooks::PreToolUseRequest;
use codex_hooks::SessionStartOutcome;
Expand All @@ -15,6 +17,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::HookCompletedEvent;
use codex_protocol::protocol::HookRunSummary;
use codex_protocol::user_input::UserInput;
use serde_json::Value;

use crate::codex::Session;
use crate::codex::TurnContext;
Expand Down Expand Up @@ -92,7 +95,7 @@ pub(crate) async fn run_pending_session_start_hooks(

let request = codex_hooks::SessionStartRequest {
session_id: sess.conversation_id,
cwd: turn_context.cwd.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),
Expand Down Expand Up @@ -120,7 +123,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
let request = PreToolUseRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.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),
Expand All @@ -141,6 +144,33 @@ pub(crate) async fn run_pre_tool_use_hooks(
if should_block { block_reason } else { None }
}

pub(crate) async fn run_post_tool_use_hooks(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
tool_use_id: String,
command: String,
tool_response: Value,
) -> PostToolUseOutcome {
let request = PostToolUseRequest {
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: "Bash".to_string(),
tool_use_id,
command,
tool_response,
};
let preview_runs = sess.hooks().preview_post_tool_use(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;

let outcome = sess.hooks().run_post_tool_use(request).await;
emit_hook_completed_events(sess, turn_context, outcome.hook_events.clone()).await;
outcome
}

pub(crate) async fn run_user_prompt_submit_hooks(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
Expand All @@ -149,7 +179,7 @@ pub(crate) async fn run_user_prompt_submit_hooks(
let request = UserPromptSubmitRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.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),
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/core/src/tools/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ pub trait ToolOutput: Send {

fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;

fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what should this property contain for a notmal tool output?

Copy link
Copy Markdown
Contributor Author

@eternal-openai eternal-openai Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default it should be None. A tool only populates post_tool_use_response if it intentionally supports a stable PostToolUse payload shape

None
}

fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue {
response_input_to_code_mode_result(self.to_response_item("", payload))
}
Expand Down Expand Up @@ -158,13 +162,15 @@ impl ToolOutput for ToolSearchOutput {
pub struct FunctionToolOutput {
pub body: Vec<FunctionCallOutputContentItem>,
pub success: Option<bool>,
pub post_tool_use_response: Option<JsonValue>,
}

impl FunctionToolOutput {
pub fn from_text(text: String, success: Option<bool>) -> Self {
Self {
body: vec![FunctionCallOutputContentItem::InputText { text }],
success,
post_tool_use_response: None,
}
}

Expand All @@ -175,6 +181,7 @@ impl FunctionToolOutput {
Self {
body: content,
success,
post_tool_use_response: None,
}
}

Expand All @@ -197,6 +204,10 @@ impl ToolOutput for FunctionToolOutput {
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
function_tool_response(call_id, payload, self.body.clone(), self.success)
}

fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
self.post_tool_use_response.clone()
}
}

pub struct ApplyPatchToolOutput {
Expand Down Expand Up @@ -305,6 +316,14 @@ impl ToolOutput for ExecCommandToolOutput {
)
}

fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
if self.process_id.is_some() || self.session_command.is_none() {
return None;
}

Some(JsonValue::String(self.truncated_output()))
}

fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
#[derive(Serialize)]
struct UnifiedExecCodeModeResult {
Expand Down
75 changes: 74 additions & 1 deletion codex-rs/core/src/tools/handlers/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use async_trait::async_trait;
use codex_protocol::ThreadId;
use codex_protocol::models::ShellCommandToolCallParams;
use codex_protocol::models::ShellToolCallParams;
use serde_json::Value as JsonValue;
use std::sync::Arc;

use crate::codex::TurnContext;
Expand All @@ -16,16 +17,20 @@ use crate::shell::Shell;
use crate::skills::maybe_emit_implicit_skill_invocation;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
use crate::tools::handlers::apply_granted_turn_permissions;
use crate::tools::handlers::apply_patch::intercept_apply_patch;
use crate::tools::handlers::implicit_granted_permissions;
use crate::tools::handlers::normalize_and_validate_additional_permissions;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::orchestrator::ToolOrchestrator;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRequest;
Expand All @@ -48,6 +53,28 @@ pub struct ShellCommandHandler {
backend: ShellCommandBackend,
}

fn shell_payload_command(payload: &ToolPayload) -> Option<String> {
match payload {
ToolPayload::Function { arguments } => parse_arguments::<ShellToolCallParams>(arguments)
.ok()
.map(|params| codex_shell_command::parse_command::shlex_join(&params.command)),
ToolPayload::LocalShell { params } => Some(codex_shell_command::parse_command::shlex_join(
&params.command,
)),
_ => None,
}
}

fn shell_command_payload_command(payload: &ToolPayload) -> Option<String> {
let ToolPayload::Function { arguments } = payload else {
return None;
};

parse_arguments::<ShellCommandToolCallParams>(arguments)
.ok()
.map(|params| params.command)
}

struct RunExecLikeArgs {
tool_name: String,
exec_params: ExecParams,
Expand Down Expand Up @@ -178,6 +205,23 @@ impl ToolHandler for ShellHandler {
}
}

fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_payload_command(&invocation.payload).map(|command| PreToolUsePayload { command })
}

fn post_tool_use_payload(
&self,
call_id: &str,
payload: &ToolPayload,
result: &dyn ToolOutput,
) -> Option<PostToolUsePayload> {
let tool_response = result.post_tool_use_response(call_id, payload)?;
Some(PostToolUsePayload {
command: shell_payload_command(payload)?,
tool_response,
})
}

async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
Expand Down Expand Up @@ -268,6 +312,24 @@ impl ToolHandler for ShellCommandHandler {
.unwrap_or(true)
}

fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_command_payload_command(&invocation.payload)
.map(|command| PreToolUsePayload { command })
}

fn post_tool_use_payload(
&self,
call_id: &str,
payload: &ToolPayload,
result: &dyn ToolOutput,
) -> Option<PostToolUsePayload> {
let tool_response = result.post_tool_use_response(call_id, payload)?;
Some(PostToolUsePayload {
command: shell_command_payload_command(payload)?,
tool_response,
})
}

async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
Expand Down Expand Up @@ -492,8 +554,19 @@ impl ShellHandler {
&call_id,
/*turn_diff_tracker*/ None,
);
let post_tool_use_response = out
.as_ref()
.ok()
.map(|output| crate::tools::format_exec_output_str(output, turn.truncation_policy))
.map(JsonValue::String);
let content = emitter.finish(event_ctx, out).await?;
Ok(FunctionToolOutput::from_text(content, Some(true)))
Ok(FunctionToolOutput {
body: vec![
codex_protocol::models::FunctionCallOutputContentItem::InputText { text: content },
],
success: Some(true),
post_tool_use_response,
})
}
}

Expand Down
Loading
Loading