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
+3 -3
View File
@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use crate::HookEventsToml;
use crate::HooksToml;
use crate::permissions_toml::PermissionsToml;
use crate::profile_toml::ConfigProfile;
use crate::types::AnalyticsConfigToml;
@@ -343,8 +343,8 @@ pub struct ConfigToml {
/// User-level skill config entries keyed by SKILL.md path.
pub skills: Option<SkillsConfig>,
/// Lifecycle hooks configured inline in TOML.
pub hooks: Option<HookEventsToml>,
/// Lifecycle hooks configured inline in TOML plus user-level overrides.
pub hooks: Option<HooksToml>,
/// User-level plugin config entries keyed by plugin name.
#[serde(default)]
+15
View File
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
@@ -12,6 +13,20 @@ pub struct HooksFile {
pub hooks: HookEventsToml,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct HooksToml {
#[serde(flatten)]
pub events: HookEventsToml,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub state: BTreeMap<String, HookStateToml>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct HookStateToml {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct HookEventsToml {
#[serde(rename = "PreToolUse", default)]
+45
View File
@@ -1,8 +1,11 @@
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use super::HookEventsToml;
use super::HookHandlerConfig;
use super::HooksFile;
use super::HooksToml;
use super::ManagedHooksRequirementsToml;
use super::MatcherGroup;
@@ -81,6 +84,48 @@ statusMessage = "checking"
);
}
#[test]
fn hooks_toml_deserializes_inline_events_and_state_map() {
let parsed: HooksToml = toml::from_str(
r#"
[state."/tmp/hooks.json:pre_tool_use:0:0"]
enabled = false
[[PreToolUse]]
matcher = "^Bash$"
[[PreToolUse.hooks]]
type = "command"
command = "python3 /tmp/pre.py"
"#,
)
.expect("hooks TOML should deserialize");
assert_eq!(
parsed,
HooksToml {
events: HookEventsToml {
pre_tool_use: vec![MatcherGroup {
matcher: Some("^Bash$".to_string()),
hooks: vec![HookHandlerConfig::Command {
command: "python3 /tmp/pre.py".to_string(),
timeout_sec: None,
r#async: false,
status_message: None,
}],
}],
..Default::default()
},
state: BTreeMap::from([(
"/tmp/hooks.json:pre_tool_use:0:0".to_string(),
super::HookStateToml {
enabled: Some(false),
},
)]),
}
);
}
#[test]
fn managed_hooks_requirements_flatten_hook_events() {
let parsed: ManagedHooksRequirementsToml = toml::from_str(
+2
View File
@@ -73,7 +73,9 @@ pub use diagnostics::io_error_from_config_error;
pub use fingerprint::version_for_toml;
pub use hook_config::HookEventsToml;
pub use hook_config::HookHandlerConfig;
pub use hook_config::HookStateToml;
pub use hook_config::HooksFile;
pub use hook_config::HooksToml;
pub use hook_config::ManagedHooksRequirementsToml;
pub use hook_config::MatcherGroup;
pub use host_name::host_name;