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 authored and copyberry committed 2026-08-08 04:57:06 +00:00
1 parent 6f647caa9b
commit 3aae5d885b
23 files changed
+75 -3

No files matched your search

@@ -11794,6 +11794,14 @@
"eventName": { "eventName": {
"$ref": "#/definitions/v2/HookEventName" "$ref": "#/definitions/v2/HookEventName"
}, },
"executionMode": {
"allOf": [
{
"$ref": "#/definitions/v2/HookExecutionMode"
}
],
"default": "sync"
},
"handlerType": { "handlerType": {
"$ref": "#/definitions/v2/HookHandlerType" "$ref": "#/definitions/v2/HookHandlerType"
}, },
@@ -8070,6 +8070,14 @@
"eventName": { "eventName": {
"$ref": "#/definitions/HookEventName" "$ref": "#/definitions/HookEventName"
}, },
"executionMode": {
"allOf": [
{
"$ref": "#/definitions/HookExecutionMode"
}
],
"default": "sync"
},
"handlerType": { "handlerType": {
"$ref": "#/definitions/HookHandlerType" "$ref": "#/definitions/HookHandlerType"
}, },
@@ -36,6 +36,13 @@
], ],
"type": "string" "type": "string"
}, },
"HookExecutionMode": {
"enum": [
"sync",
"async"
],
"type": "string"
},
"HookHandlerType": { "HookHandlerType": {
"enum": [ "enum": [
"command", "command",
@@ -74,6 +81,14 @@
"eventName": { "eventName": {
"$ref": "#/definitions/HookEventName" "$ref": "#/definitions/HookEventName"
}, },
"executionMode": {
"allOf": [
{
"$ref": "#/definitions/HookExecutionMode"
}
],
"default": "sync"
},
"handlerType": { "handlerType": {
"$ref": "#/definitions/HookHandlerType" "$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. // 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 { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { HookEventName } from "./HookEventName"; import type { HookEventName } from "./HookEventName";
import type { HookExecutionMode } from "./HookExecutionMode";
import type { HookHandlerType } from "./HookHandlerType"; import type { HookHandlerType } from "./HookHandlerType";
import type { HookSource } from "./HookSource"; import type { HookSource } from "./HookSource";
import type { HookTrustStatus } from "./HookTrustStatus"; 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. * Configured `additionalContext` spill threshold.
* `null` uses 2,500 tokens; `0` disables spilling. * `null` uses 2,500 tokens; `0` disables spilling.
@@ -28,8 +28,11 @@ v2_enum_from_core!(
); );
v2_enum_from_core!( v2_enum_from_core!(
#[derive(Default)]
pub enum HookExecutionMode from CoreHookExecutionMode { pub enum HookExecutionMode from CoreHookExecutionMode {
Sync, Async #[default]
Sync,
Async
} }
); );
@@ -1,5 +1,6 @@
use super::AppSummary; use super::AppSummary;
use super::HookEventName; use super::HookEventName;
use super::HookExecutionMode;
use super::HookHandlerType; use super::HookHandlerType;
use super::HookSource; use super::HookSource;
use super::HookTrustStatus; use super::HookTrustStatus;
@@ -524,6 +525,8 @@ pub struct HookMetadata {
pub key: String, pub key: String,
pub event_name: HookEventName, pub event_name: HookEventName,
pub handler_type: HookHandlerType, pub handler_type: HookHandlerType,
#[serde(default)]
pub execution_mode: HookExecutionMode,
pub matcher: Option<String>, pub matcher: Option<String>,
pub command: Option<String>, pub command: Option<String>,
pub timeout_sec: u64, 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. 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. 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 ```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", "key": "/Users/me/.codex/config.toml:pre_tool_use:0:0",
"eventName": "pre_tool_use", "eventName": "pre_tool_use",
"handlerType": "command", "handlerType": "command",
"executionMode": "sync",
"isManaged": false, "isManaged": false,
"matcher": "Bash", "matcher": "Bash",
"command": "python3 /Users/me/hook.py", "command": "python3 /Users/me/hook.py",
@@ -70,6 +70,7 @@ fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec<HookMetadata> {
key: hook.key.clone(), key: hook.key.clone(),
event_name: hook.event_name.into(), event_name: hook.event_name.into(),
handler_type: hook.handler_type.into(), handler_type: hook.handler_type.into(),
execution_mode: hook.execution_mode.into(),
matcher: hook.matcher.clone(), matcher: hook.matcher.clone(),
command: hook.command.clone(), command: hook.command.clone(),
timeout_sec: hook.timeout_sec, 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::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit; use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::HookEventName; use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookHandlerType; use codex_app_server_protocol::HookHandlerType;
use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource; use codex_app_server_protocol::HookSource;
@@ -45,6 +46,7 @@ fn command_hook_hash(
matcher: Option<&str>, matcher: Option<&str>,
command: &str, command: &str,
timeout_sec: u64, timeout_sec: u64,
execution_mode: HookExecutionMode,
status_message: Option<&str>, status_message: Option<&str>,
additional_context_limit: Option<usize>, additional_context_limit: Option<usize>,
) -> String { ) -> String {
@@ -56,7 +58,7 @@ fn command_hook_hash(
command: command.to_string(), command: command.to_string(),
command_windows: None, command_windows: None,
timeout_sec: Some(timeout_sec), timeout_sec: Some(timeout_sec),
r#async: false, r#async: execution_mode == HookExecutionMode::Async,
status_message: status_message.map(ToOwned::to_owned), status_message: status_message.map(ToOwned::to_owned),
additional_context_limit, additional_context_limit,
}], }],
@@ -80,6 +82,7 @@ matcher = "Bash"
type = "command" type = "command"
command = "python3 /tmp/listed-hook.py" command = "python3 /tmp/listed-hook.py"
timeout = 5 timeout = 5
async = true
statusMessage = "running listed hook" statusMessage = "running listed hook"
additionalContextLimit = 4096 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()), key: format!("{}:pre_tool_use:0:0", config_path.as_path().display()),
event_name: HookEventName::PreToolUse, event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command, handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Async,
matcher: Some("Bash".to_string()), matcher: Some("Bash".to_string()),
command: Some("python3 /tmp/listed-hook.py".to_string()), command: Some("python3 /tmp/listed-hook.py".to_string()),
timeout_sec: 5, timeout_sec: 5,
@@ -177,6 +181,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
Some("Bash"), Some("Bash"),
"python3 /tmp/listed-hook.py", "python3 /tmp/listed-hook.py",
/*timeout_sec*/ 5, /*timeout_sec*/ 5,
HookExecutionMode::Async,
Some("running listed hook"), Some("running listed hook"),
/*additional_context_limit*/ Some(4_096), /*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(), key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
event_name: HookEventName::PreToolUse, event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command, handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
matcher: Some("Bash".to_string()), matcher: Some("Bash".to_string()),
command: Some("echo plugin hook".to_string()), command: Some("echo plugin hook".to_string()),
timeout_sec: 7, timeout_sec: 7,
@@ -256,6 +262,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
Some("Bash"), Some("Bash"),
"echo plugin hook", "echo plugin hook",
/*timeout_sec*/ 7, /*timeout_sec*/ 7,
HookExecutionMode::Sync,
Some("running plugin hook"), Some("running plugin hook"),
/*additional_context_limit*/ None, /*additional_context_limit*/ None,
), ),
@@ -436,6 +443,7 @@ timeout = 5
), ),
event_name: HookEventName::PreToolUse, event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command, handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
matcher: Some("Bash".to_string()), matcher: Some("Bash".to_string()),
command: Some("echo project hook".to_string()), command: Some("echo project hook".to_string()),
timeout_sec: 5, timeout_sec: 5,
@@ -452,6 +460,7 @@ timeout = 5
Some("Bash"), Some("Bash"),
"echo project hook", "echo project hook",
/*timeout_sec*/ 5, /*timeout_sec*/ 5,
HookExecutionMode::Sync,
/*status_message*/ None, /*status_message*/ None,
/*additional_context_limit*/ None, /*additional_context_limit*/ None,
), ),
+1
View File
@@ -558,6 +558,7 @@ fn append_matcher_groups(
is_managed: source.is_managed, is_managed: source.is_managed,
current_hash, current_hash,
trust_status, trust_status,
execution_mode,
}); });
if enabled if enabled
&& (source.bypass_hook_trust && (source.bypass_hook_trust
+1
View File
@@ -108,6 +108,7 @@ pub struct HookListEntry {
pub is_managed: bool, pub is_managed: bool,
pub current_hash: String, pub current_hash: String,
pub trust_status: HookTrustStatus, pub trust_status: HookTrustStatus,
pub execution_mode: codex_protocol::protocol::HookExecutionMode,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -1,4 +1,5 @@
use codex_app_server_protocol::HookEventName; use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource; use codex_app_server_protocol::HookSource;
use codex_app_server_protocol::HookTrustStatus; use codex_app_server_protocol::HookTrustStatus;
@@ -492,6 +493,11 @@ impl HooksBrowserView {
width, width,
Some(MAX_COMMAND_DETAIL_LINES), 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))); lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec)));
if let Some(limit) = hook.additional_context_limit { if let Some(limit) = hook.additional_context_limit {
let value = if limit == 0 { let value = if limit == 0 {
@@ -883,6 +889,7 @@ mod tests {
use crate::test_support::test_path_display; use crate::test_support::test_path_display;
use codex_app_server_protocol::HookErrorInfo; use codex_app_server_protocol::HookErrorInfo;
use codex_app_server_protocol::HookEventName; use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookHandlerType; use codex_app_server_protocol::HookHandlerType;
use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource; use codex_app_server_protocol::HookSource;
@@ -947,6 +954,7 @@ mod tests {
key: key.to_string(), key: key.to_string(),
event_name, event_name,
handler_type: HookHandlerType::Command, handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
is_managed, is_managed,
matcher: Some("Bash".to_string()), matcher: Some("Bash".to_string()),
command: Some(command.to_string()), command: Some(command.to_string()),
@@ -1142,6 +1150,7 @@ mod tests {
/*display_order*/ 0, /*display_order*/ 0,
); );
untrusted_hook.trust_status = HookTrustStatus::Untrusted; untrusted_hook.trust_status = HookTrustStatus::Untrusted;
untrusted_hook.execution_mode = HookExecutionMode::Async;
let mut view = HooksBrowserView::new( let mut view = HooksBrowserView::new(
vec![untrusted_hook], vec![untrusted_hook],
Vec::new(), Vec::new(),
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source User config - /tmp/hooks.json Source User config - /tmp/hooks.json
Command /tmp/pre-tool-use.sh Command /tmp/pre-tool-use.sh
Mode Sync
Timeout 30s Timeout 30s
Context unlimited Context unlimited
Trust Trusted Trust Trusted
@@ -14,6 +14,7 @@ expression: "render_lines(&view, 44)"
Command one two three four five six Command one two three four five six
seven eight nine ten eleven seven eight nine ten eleven
twelve thirteen fourteen… twelve thirteen fourteen…
Mode Sync
Timeout 30s Timeout 30s
Trust Trusted Trust Trusted
@@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source Plugin - superpowers@openai-curated Source Plugin - superpowers@openai-curated
Command ${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh Command ${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh
Mode Sync
Timeout 30s Timeout 30s
Trust Trusted Trust Trusted
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source Admin config Source Admin config
Command /enterprise/hooks/permission-check.sh Command /enterprise/hooks/permission-check.sh
Mode Sync
Timeout 30s Timeout 30s
Trust Managed Trust Managed
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source User config - /tmp/hooks.json Source User config - /tmp/hooks.json
Command /tmp/pre-tool-use-check.sh Command /tmp/pre-tool-use-check.sh
Mode Sync
Timeout 30s Timeout 30s
Trust New hook - review required Trust New hook - review required
@@ -19,6 +19,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source User config - /tmp/hooks.json Source User config - /tmp/hooks.json
Command /tmp/hook-8.sh Command /tmp/hook-8.sh
Mode Sync
Timeout 30s Timeout 30s
Trust Trusted Trust Trusted
@@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source Admin config Source Admin config
Command /enterprise/hooks/pre-tool-use-2.sh Command /enterprise/hooks/pre-tool-use-2.sh
Mode Sync
Timeout 30s Timeout 30s
Trust Managed Trust Managed
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
Matcher Bash Matcher Bash
Source User config - /tmp/hooks.json Source User config - /tmp/hooks.json
Command ~/bin/untrusted.sh Command ~/bin/untrusted.sh
Mode Async
Timeout 30s Timeout 30s
Trust New hook - review required Trust New hook - review required
+2
View File
@@ -305,6 +305,7 @@ mod tests {
use crate::test_support::PathBufExt; use crate::test_support::PathBufExt;
use crate::test_support::test_path_buf; use crate::test_support::test_path_buf;
use codex_app_server_protocol::HookEventName; use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookExecutionMode;
use codex_app_server_protocol::HookHandlerType; use codex_app_server_protocol::HookHandlerType;
use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource; use codex_app_server_protocol::HookSource;
@@ -320,6 +321,7 @@ mod tests {
key: key.to_string(), key: key.to_string(),
event_name: HookEventName::PreToolUse, event_name: HookEventName::PreToolUse,
handler_type: HookHandlerType::Command, handler_type: HookHandlerType::Command,
execution_mode: HookExecutionMode::Sync,
is_managed: false, is_managed: false,
matcher: Some("Bash".to_string()), matcher: Some("Bash".to_string()),
command: Some("/tmp/hook.sh".to_string()), command: Some("/tmp/hook.sh".to_string()),