Add persisted hook enablement state (#19840)

## Why

After `hooks/list` exposes the hook inventory, clients need a way to
persist user hook preferences, make those changes effective in
already-open sessions, and distinguish user-controllable hooks from
managed requirements without adding another bespoke app-server write
API.

## What

- Extends `hooks/list` entries with effective `enabled` state.
- Persists user-level hook state under `hooks.state.<hook-id>` so the
model can grow beyond a single boolean over time.
- Uses the existing `config/batchWrite` path for hook state updates
instead of introducing a dedicated hook write RPC.
- Refreshes live session hook engines after config writes so
already-open threads observe updated enablement without a restart.

## Stack

1. openai/codex#19705
2. openai/codex#19778
3. This PR - openai/codex#19840
4. openai/codex#19882

## Reviewer Notes

The generated schema files account for much of the raw diff. The core
behavior is in:

- `hooks/src/config_rules.rs`, which resolves per-hook user state from
the config layer stack.
- `hooks/src/engine/discovery.rs`, which projects effective enablement
into `hooks/list` from source-derived managedness.
- `config/src/hook_config.rs`, which defines the new `hooks.state`
representation.
- `core/src/session/mod.rs`, which rebuilds live hook state after user
config reloads.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Abhinav
2026-04-30 04:46:32 +00:00
committed by GitHub
co-authored by Codex
parent ac4332c05b
commit 8f3c06cc97
39 changed files with 1212 additions and 181 deletions
+28 -11
View File
@@ -116,13 +116,13 @@ pub(crate) async fn run_pending_session_start_hooks(
permission_mode: hook_permission_mode(turn_context),
source: session_start_source,
};
let preview_runs = sess.hooks().preview_session_start(&request);
let hooks = sess.hooks();
let preview_runs = hooks.preview_session_start(&request);
run_context_injecting_hook(
sess,
turn_context,
preview_runs,
sess.hooks()
.run_session_start(request, Some(turn_context.sub_id.clone())),
hooks.run_session_start(request, Some(turn_context.sub_id.clone())),
)
.await
.record_additional_contexts(sess, turn_context)
@@ -153,14 +153,15 @@ pub(crate) async fn run_pre_tool_use_hooks(
tool_use_id,
tool_input: tool_input.clone(),
};
let preview_runs = sess.hooks().preview_pre_tool_use(&request);
let hooks = sess.hooks();
let preview_runs = hooks.preview_pre_tool_use(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;
let PreToolUseOutcome {
hook_events,
should_block,
block_reason,
} = sess.hooks().run_pre_tool_use(request).await;
} = hooks.run_pre_tool_use(request).await;
emit_hook_completed_events(sess, turn_context, hook_events).await;
if should_block {
@@ -202,13 +203,14 @@ pub(crate) async fn run_permission_request_hooks(
run_id_suffix: run_id_suffix.to_string(),
tool_input: payload.tool_input,
};
let preview_runs = sess.hooks().preview_permission_request(&request);
let hooks = sess.hooks();
let preview_runs = 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;
} = hooks.run_permission_request(request).await;
emit_hook_completed_events(sess, turn_context, hook_events).await;
decision
@@ -242,10 +244,11 @@ pub(crate) async fn run_post_tool_use_hooks(
tool_input,
tool_response,
};
let preview_runs = sess.hooks().preview_post_tool_use(&request);
let hooks = sess.hooks();
let preview_runs = 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;
let outcome = hooks.run_post_tool_use(request).await;
emit_hook_completed_events(sess, turn_context, outcome.hook_events.clone()).await;
outcome
}
@@ -264,12 +267,13 @@ pub(crate) async fn run_user_prompt_submit_hooks(
permission_mode: hook_permission_mode(turn_context),
prompt,
};
let preview_runs = sess.hooks().preview_user_prompt_submit(&request);
let hooks = sess.hooks();
let preview_runs = hooks.preview_user_prompt_submit(&request);
run_context_injecting_hook(
sess,
turn_context,
preview_runs,
sess.hooks().run_user_prompt_submit(request),
hooks.run_user_prompt_submit(request),
)
.await
}
@@ -474,6 +478,7 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
HookSource::Mdm => "mdm",
HookSource::SessionFlags => "session_flags",
HookSource::Plugin => "plugin",
HookSource::CloudRequirements => "cloud_requirements",
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
HookSource::Unknown => "unknown",
@@ -605,6 +610,18 @@ mod tests {
("status", "blocked"),
]
);
let cloud_requirements =
sample_hook_run(HookRunStatus::Blocked, HookSource::CloudRequirements);
assert_eq!(
hook_run_metric_tags(&cloud_requirements),
[
("hook_name", "Stop"),
("source", "cloud_requirements"),
("status", "blocked"),
]
);
}
#[test]