Expose execution mode in hook listings (#37538)

## What changed

- Add `executionMode` to `HookMetadata` returned by `hooks/list`, with `sync` as the default for compatibility.
- Propagate each discovered hook's `sync` or `async` mode through the app-server protocol and generated schemas.
- Show the execution mode in the TUI hooks browser.

## Testing

- Cover async and default-sync modes in `hooks/list` tests and TUI snapshots.

GitOrigin-RevId: 9c9f2890798fe417face76c15847616b2dea9db4
This commit is contained in:
Abhinav
2026-08-08 04:57:06 +00:00
committed by copyberry
parent 6f647caa9b
commit 3aae5d885b
23 changed files with 75 additions and 3 deletions
@@ -11794,6 +11794,14 @@
"eventName": {
"$ref": "#/definitions/v2/HookEventName"
},
"executionMode": {
"allOf": [
{
"$ref": "#/definitions/v2/HookExecutionMode"
}
],
"default": "sync"
},
"handlerType": {
"$ref": "#/definitions/v2/HookHandlerType"
},
@@ -8070,6 +8070,14 @@
"eventName": {
"$ref": "#/definitions/HookEventName"
},
"executionMode": {
"allOf": [
{
"$ref": "#/definitions/HookExecutionMode"
}
],
"default": "sync"
},
"handlerType": {
"$ref": "#/definitions/HookHandlerType"
},
@@ -36,6 +36,13 @@
],
"type": "string"
},
"HookExecutionMode": {
"enum": [
"sync",
"async"
],
"type": "string"
},
"HookHandlerType": {
"enum": [
"command",
@@ -74,6 +81,14 @@
"eventName": {
"$ref": "#/definitions/HookEventName"
},
"executionMode": {
"allOf": [
{
"$ref": "#/definitions/HookExecutionMode"
}
],
"default": "sync"
},
"handlerType": {
"$ref": "#/definitions/HookHandlerType"
},
@@ -3,11 +3,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { HookEventName } from "./HookEventName";
import type { HookExecutionMode } from "./HookExecutionMode";
import type { HookHandlerType } from "./HookHandlerType";
import type { HookSource } from "./HookSource";
import type { HookTrustStatus } from "./HookTrustStatus";
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null,
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null,
/**
* Configured `additionalContext` spill threshold.
* `null` uses 2,500 tokens; `0` disables spilling.
@@ -28,8 +28,11 @@ v2_enum_from_core!(
);
v2_enum_from_core!(
#[derive(Default)]
pub enum HookExecutionMode from CoreHookExecutionMode {
Sync, Async
#[default]
Sync,
Async
}
);
@@ -1,5 +1,6 @@
use super::AppSummary;
use super::HookEventName;
use super::HookExecutionMode;
use super::HookHandlerType;
use super::HookSource;
use super::HookTrustStatus;
@@ -524,6 +525,8 @@ pub struct HookMetadata {
pub key: String,
pub event_name: HookEventName,
pub handler_type: HookHandlerType,
#[serde(default)]
pub execution_mode: HookExecutionMode,
pub matcher: Option<String>,
pub command: Option<String>,
pub timeout_sec: u64,
+3
View File
@@ -1926,6 +1926,8 @@ For linked Git worktrees, project hook declarations come from the matching `.cod
Hooks are returned even when disabled so clients can render and re-enable them. User-controlled state lives under `hooks.state`. Managed hooks are non-configurable, and user entries for managed hook keys are ignored during loading.
`executionMode` reports how a command hook runs. `sync` hooks participate in the current operation, while `async` hooks run in the background and deliver informational output through the existing steer-based injection path. Output is injected immediately into an active turn or persisted without starting a new turn when the session is idle.
For unmanaged hooks, `currentHash` and `trustStatus` describe whether the current definition is first-seen, approved, or changed since approval. Only trusted unmanaged hooks become runnable. Hook keys combine the source identity with a trailing event/group/handler selector that is currently positional.
```json
@@ -1948,6 +1950,7 @@ For unmanaged hooks, `currentHash` and `trustStatus` describe whether the curren
"key": "/Users/me/.codex/config.toml:pre_tool_use:0:0",
"eventName": "pre_tool_use",
"handlerType": "command",
"executionMode": "sync",
"isManaged": false,
"matcher": "Bash",
"command": "python3 /Users/me/hook.py",
@@ -70,6 +70,7 @@ fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec<HookMetadata> {
key: hook.key.clone(),
event_name: hook.event_name.into(),
handler_type: hook.handler_type.into(),
execution_mode: hook.execution_mode.into(),
matcher: hook.matcher.clone(),
command: hook.command.clone(),
timeout_sec: hook.timeout_sec,
@@ -8,6 +8,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookHandlerType;
use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource;
@@ -45,6 +46,7 @@ fn command_hook_hash(
matcher: Option<&str>,
command: &str,
timeout_sec: u64,
execution_mode: HookExecutionMode,
status_message: Option<&str>,
additional_context_limit: Option<usize>,
) -> String {
@@ -56,7 +58,7 @@ fn command_hook_hash(
command: command.to_string(),
command_windows: None,
timeout_sec: Some(timeout_sec),
r#async: false,
r#async: execution_mode == HookExecutionMode::Async,
status_message: status_message.map(ToOwned::to_owned),
additional_context_limit,
}],
@@ -80,6 +82,7 @@ matcher = "Bash"
type = "command"
command = "python3 /tmp/listed-hook.py"
timeout = 5
async = true
statusMessage = "running listed hook"
additionalContextLimit = 4096
"#,
@@ -161,6 +164,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
key: format!("{}:pre_tool_use:0:0", config_path.as_path().display()),
event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Async,
matcher: Some("Bash".to_string()),
command: Some("python3 /tmp/listed-hook.py".to_string()),
timeout_sec: 5,
@@ -177,6 +181,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
Some("Bash"),
"python3 /tmp/listed-hook.py",
/*timeout_sec*/ 5,
HookExecutionMode::Async,
Some("running listed hook"),
/*additional_context_limit*/ Some(4_096),
),
@@ -240,6 +245,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
matcher: Some("Bash".to_string()),
command: Some("echo plugin hook".to_string()),
timeout_sec: 7,
@@ -256,6 +262,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
Some("Bash"),
"echo plugin hook",
/*timeout_sec*/ 7,
HookExecutionMode::Sync,
Some("running plugin hook"),
/*additional_context_limit*/ None,
),
@@ -436,6 +443,7 @@ timeout = 5
),
event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
matcher: Some("Bash".to_string()),
command: Some("echo project hook".to_string()),
timeout_sec: 5,
@@ -452,6 +460,7 @@ timeout = 5
Some("Bash"),
"echo project hook",
/*timeout_sec*/ 5,
HookExecutionMode::Sync,
/*status_message*/ None,
/*additional_context_limit*/ None,
),
+1
View File
@@ -558,6 +558,7 @@ fn append_matcher_groups(
is_managed: source.is_managed,
current_hash,
trust_status,
execution_mode,
});
if enabled
&& (source.bypass_hook_trust
+1
View File
@@ -108,6 +108,7 @@ pub struct HookListEntry {
pub is_managed: bool,
pub current_hash: String,
pub trust_status: HookTrustStatus,
pub execution_mode: codex_protocol::protocol::HookExecutionMode,
}
#[derive(Clone)]
@@ -1,4 +1,5 @@
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource;
use codex_app_server_protocol::HookTrustStatus;
@@ -492,6 +493,11 @@ impl HooksBrowserView {
width,
Some(MAX_COMMAND_DETAIL_LINES),
));
let execution_mode = match hook.execution_mode {
HookExecutionMode::Sync => "Sync",
HookExecutionMode::Async => "Async",
};
lines.push(detail_line("Mode", execution_mode));
lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec)));
if let Some(limit) = hook.additional_context_limit {
let value = if limit == 0 {
@@ -883,6 +889,7 @@ mod tests {
use crate::test_support::test_path_display;
use codex_app_server_protocol::HookErrorInfo;
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookHandlerType;
use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource;
@@ -947,6 +954,7 @@ mod tests {
key: key.to_string(),
event_name,
handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
is_managed,
matcher: Some("Bash".to_string()),
command: Some(command.to_string()),
@@ -1142,6 +1150,7 @@ mod tests {
/*display_order*/ 0,
);
untrusted_hook.trust_status = HookTrustStatus::Untrusted;
untrusted_hook.execution_mode = HookExecutionMode::Async;
let mut view = HooksBrowserView::new(
vec![untrusted_hook],
Vec::new(),
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source User config - /tmp/hooks.json
Command /tmp/pre-tool-use.sh
Mode Sync
Timeout 30s
Context unlimited
Trust Trusted
@@ -14,6 +14,7 @@ expression: "render_lines(&view, 44)"
Command one two three four five six
seven eight nine ten eleven
twelve thirteen fourteen…
Mode Sync
Timeout 30s
Trust Trusted
@@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source Plugin - superpowers@openai-curated
Command ${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh
Mode Sync
Timeout 30s
Trust Trusted
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source Admin config
Command /enterprise/hooks/permission-check.sh
Mode Sync
Timeout 30s
Trust Managed
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source User config - /tmp/hooks.json
Command /tmp/pre-tool-use-check.sh
Mode Sync
Timeout 30s
Trust New hook - review required
@@ -19,6 +19,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source User config - /tmp/hooks.json
Command /tmp/hook-8.sh
Mode Sync
Timeout 30s
Trust Trusted
@@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source Admin config
Command /enterprise/hooks/pre-tool-use-2.sh
Mode Sync
Timeout 30s
Trust Managed
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash
Source User config - /tmp/hooks.json
Command ~/bin/untrusted.sh
Mode Async
Timeout 30s
Trust New hook - review required
+2
View File
@@ -305,6 +305,7 @@ mod tests {
use crate::test_support::PathBufExt;
use crate::test_support::test_path_buf;
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookHandlerType;
use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource;
@@ -320,6 +321,7 @@ mod tests {
key: key.to_string(),
event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
is_managed: false,
matcher: Some("Bash".to_string()),
command: Some("/tmp/hook.sh".to_string()),