Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
b2f2c98
Discover hooks bundled with plugins
abhinav-oai Apr 26, 2026
d2097f9
Remove plugin hook load warnings from discovery
abhinav-oai Apr 26, 2026
6655134
Refactor plugin hook file loading
abhinav-oai Apr 26, 2026
5aed78a
Fix plugin hook test argument comment
abhinav-oai Apr 26, 2026
803c16f
Move plugin loader tests out of implementation
abhinav-oai Apr 26, 2026
108dc35
Inline plugin hook relative path
abhinav-oai Apr 27, 2026
d5539f4
Add hook listing and config APIs
abhinav-oai Apr 27, 2026
38c5e3a
Document hook config APIs
abhinav-oai Apr 27, 2026
6d03af7
Refactor plugin hook loader test assertions
abhinav-oai Apr 27, 2026
06d95b2
Reduce hooks list PR to inventory only
abhinav-oai Apr 27, 2026
b1307c2
Add hook config write support
abhinav-oai Apr 27, 2026
a6df152
Simplify shared cwd config loading
abhinav-oai Apr 27, 2026
50ed9a4
Merge branch 'main' into dev/abhinav/plugin-hooks-discovery
abhinav-oai Apr 27, 2026
2c7a78c
Merge remote-tracking branch 'origin/dev/abhinav/plugin-hooks-discove…
abhinav-oai Apr 27, 2026
44789f9
Add hooks browser menu
abhinav-oai Apr 27, 2026
7bf4986
Clean up hooks browser metadata
abhinav-oai Apr 27, 2026
30f22ed
Fix hooks browser list navigation
abhinav-oai Apr 27, 2026
0a9499d
Tighten hooks browser tall layouts
abhinav-oai Apr 27, 2026
7656221
Simplify hook source display
abhinav-oai Apr 27, 2026
fbe0bf9
Merge branch 'main' into dev/abhinav/plugin-hooks-discovery
abhinav-oai Apr 27, 2026
5622cb9
Cap hook command details
abhinav-oai Apr 27, 2026
4f04235
Simplify hook detail wrapping
abhinav-oai Apr 27, 2026
3197091
Polish hook source display
abhinav-oai Apr 27, 2026
05a5d5d
Fix hooks browser CI
abhinav-oai Apr 27, 2026
1c82afb
Merge branch 'dev/abhinav/plugin-hooks-discovery' into dev/abhinav/ho…
abhinav-oai Apr 27, 2026
98efa54
Trim hooks list metadata
abhinav-oai Apr 28, 2026
85be136
Gate plugin hook loading
abhinav-oai Apr 28, 2026
e0ff2ad
Simplify hooks list plumbing
abhinav-oai Apr 28, 2026
2c4c2c1
Support plugin hook path substitution and data dirs
abhinav-oai Apr 28, 2026
ef0f68b
Merge remote-tracking branch 'origin/dev/abhinav/plugin-hooks-discove…
abhinav-oai Apr 28, 2026
0474550
Merge remote-tracking branch 'origin/dev/abhinav/hooks-list-config' i…
abhinav-oai Apr 28, 2026
e27ed50
Merge remote-tracking branch 'origin/dev/abhinav/hooks-config-write' …
abhinav-oai Apr 28, 2026
d1f7d69
Fix hooks tests after base merge
abhinav-oai Apr 28, 2026
3261bee
Clean up hooks browser view
abhinav-oai Apr 28, 2026
e92d897
tui: show hook list diagnostics in browser
abhinav-oai Apr 28, 2026
6eb99d3
Expose managed hooks and reject user toggles
abhinav-oai Apr 28, 2026
b48e305
tui: avoid yellow in hooks diagnostics
abhinav-oai Apr 28, 2026
99edca4
tui: stabilize status snapshot sandbox setup
abhinav-oai Apr 28, 2026
5a23946
Refresh live hooks after config writes
abhinav-oai Apr 28, 2026
60283e7
Merge remote-tracking branch 'origin/dev/abhinav/hooks-config-write' …
abhinav-oai Apr 28, 2026
2f62157
Namespace hook keys by source
abhinav-oai Apr 28, 2026
bb897c1
Inline managed hook config validation
abhinav-oai Apr 28, 2026
c51a4dc
Clean up hook config model
abhinav-oai Apr 28, 2026
b9cd674
Fix plugin hook listing regressions
abhinav-oai Apr 28, 2026
32ad9b1
Store plugin hook mode on load outcome
abhinav-oai Apr 28, 2026
b1b7450
Pass plugin hook flag through helper
abhinav-oai Apr 28, 2026
d46139e
Fix argument comment lint
abhinav-oai Apr 28, 2026
2b48f64
Simplify hooks list plumbing
abhinav-oai Apr 28, 2026
425cd2d
Add hooks list plugin coverage
abhinav-oai Apr 28, 2026
ef403b2
update comment
abhinav-oai Apr 28, 2026
e92f08b
Merge remote-tracking branch 'origin/dev/abhinav/hooks-list-config' i…
abhinav-oai Apr 28, 2026
225940f
Simplify per-layer plugin loading
abhinav-oai Apr 28, 2026
ec41571
Fix argument comment lint
abhinav-oai Apr 28, 2026
2671681
Merge remote-tracking branch 'origin/dev/abhinav/hooks-config-write' …
abhinav-oai Apr 28, 2026
95f931b
Clarify plugin hook cache key
abhinav-oai Apr 28, 2026
02faa3b
Merge remote-tracking branch 'origin/main' into dev/abhinav/hooks-lis…
abhinav-oai Apr 28, 2026
2bb1247
Keep hook dispatches on one snapshot
abhinav-oai Apr 28, 2026
2efc49e
Merge base branch updates
abhinav-oai Apr 28, 2026
bfc222a
Merge branch 'main' into dev/abhinav/hooks-list-config
abhinav-oai Apr 28, 2026
ce6097a
Use config batch write for hook state
abhinav-oai Apr 28, 2026
e0124f8
Keep managed hook sources non-configurable
abhinav-oai Apr 29, 2026
1e0b0bd
Merge branch 'main' into dev/abhinav/hooks-list-config
abhinav-oai Apr 29, 2026
5744c8b
Fix hook source CI failures
abhinav-oai Apr 29, 2026
e191851
Merge remote-tracking branch 'origin/main' into dev/abhinav/hooks-lis…
abhinav-oai Apr 29, 2026
b272bb1
Merge remote-tracking branch 'origin/dev/abhinav/hooks-list-config' i…
abhinav-oai Apr 29, 2026
e0e4f62
Omit managed hook config key paths
abhinav-oai Apr 29, 2026
dbe6192
Handle hook reload review feedback
abhinav-oai Apr 29, 2026
bec8ad2
Simplify hook configurability metadata
abhinav-oai Apr 29, 2026
afbfff2
Parse hook state entries independently
abhinav-oai Apr 29, 2026
19d0a93
Document hook builder helper
abhinav-oai Apr 29, 2026
9dd8214
Simplify hook discovery state
abhinav-oai Apr 29, 2026
e94e6a1
Simplify hook test fixtures
abhinav-oai Apr 29, 2026
ca8e5d6
Merge branch 'main' into dev/abhinav/hooks-list-config
abhinav-oai Apr 29, 2026
0b46a7b
Merge branch 'dev/abhinav/hooks-list-config' into dev/abhinav/hooks-c…
abhinav-oai Apr 29, 2026
57576d1
Merge branch 'dev/abhinav/hooks-config-write' into abhinav/hooks-brow…
abhinav-oai Apr 29, 2026
319f137
Tweak hooks browser plugin labels
abhinav-oai Apr 29, 2026
8edff9f
Tweak stop hook description
abhinav-oai Apr 29, 2026
d9eec6c
update snapshot
abhinav-oai Apr 29, 2026
2c0a84a
Serialize hook toggle writes
abhinav-oai Apr 29, 2026
0981af7
Normalize hook snapshot paths in tests
abhinav-oai Apr 29, 2026
1f4dab3
Normalize hooks popup snapshot path
abhinav-oai Apr 29, 2026
a0605c6
Merge origin/main into abhinav/hooks-browser-menu
abhinav-oai Apr 30, 2026
8052447
match copies from app
abhinav-oai Apr 30, 2026
e5b9772
Merge remote-tracking branch 'origin/main' into abhinav/hooks-browser…
abhinav-oai Apr 30, 2026
691ce48
codex: fix CI failure on PR #19882
abhinav-oai Apr 30, 2026
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
25 changes: 24 additions & 1 deletion codex-rs/hooks/src/engine/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
assert!(engine.warnings().is_empty());
assert_eq!(engine.handlers.len(), 1);
assert!(engine.handlers[0].source.is_managed());
let listed = crate::list_hooks(crate::HooksConfig {
legacy_notify_argv: None,
feature_enabled: true,
config_layer_stack: Some(config_layer_stack.clone()),
plugin_hook_sources: Vec::new(),
plugin_hook_load_warnings: Vec::new(),
shell_program: None,
shell_args: Vec::new(),
});
assert!(listed.hooks[0].is_managed);
let cwd = cwd();
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
session_id: ThreadId::new(),
Expand Down Expand Up @@ -560,7 +570,7 @@ print(json.dumps({
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*config_layer_stack*/ None,
plugin_hook_sources,
plugin_hook_sources.clone(),
Vec::new(),
CommandShell {
program: String::new(),
Expand All @@ -583,6 +593,19 @@ print(json.dumps({
assert_eq!(preview.len(), 1);
assert_eq!(preview[0].source, HookSource::Plugin);
assert_eq!(preview[0].source_path, source_path);
let listed = crate::list_hooks(crate::HooksConfig {
legacy_notify_argv: None,
feature_enabled: true,
config_layer_stack: None,
plugin_hook_sources,
plugin_hook_load_warnings: Vec::new(),
shell_program: None,
shell_args: Vec::new(),
});
assert_eq!(
listed.hooks[0].plugin_id.as_deref(),
Some("demo-plugin@test-marketplace")
);

let outcome = engine
.run_pre_tool_use(PreToolUseRequest {
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;

use strum_macros::EnumIter;

use crate::AgentPath;
use crate::ThreadId;
use crate::approvals::ElicitationRequestEvent;
Expand Down Expand Up @@ -1529,7 +1531,7 @@ pub enum EventMsg {
CollabResumeEnd(CollabResumeEndEvent),
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, EnumIter)]
#[serde(rename_all = "snake_case")]
pub enum HookEventName {
PreToolUse,
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@ use codex_app_server_client::TypedRequestError;
use codex_app_server_protocol::AddCreditsNudgeCreditType;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigLayerSource;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::FeedbackUploadParams;
use codex_app_server_protocol::FeedbackUploadResponse;
use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::HooksListParams;
use codex_app_server_protocol::HooksListResponse;
use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::ListMcpServerStatusResponse;
use codex_app_server_protocol::McpServerStatus;
Expand Down Expand Up @@ -583,6 +586,9 @@ pub(crate) struct App {
// overwrite a newer toggle, even if the plugin is toggled from different
// cwd contexts.
pending_plugin_enabled_writes: HashMap<String, Option<bool>>,
// Serialize hook enablement writes per hook so stale completions cannot
// persist an older toggle after a newer one.
pending_hook_enabled_writes: HashMap<String, Option<bool>>,
}

fn active_turn_not_steerable_turn_error(error: &TypedRequestError) -> Option<AppServerTurnError> {
Expand Down Expand Up @@ -944,6 +950,7 @@ See the Codex keymap documentation for supported actions and examples."
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
};
if let Some(started) = initial_started_thread {
app.enqueue_primary_thread_session(started.session, started.turns)
Expand Down
90 changes: 90 additions & 0 deletions codex-rs/tui/src/app/background_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ impl App {
});
}

pub(super) fn fetch_hooks_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = fetch_hooks_list(request_handle, cwd.clone())
.await
.map_err(|err| err.to_string());
app_event_tx.send(AppEvent::HooksLoaded { cwd, result });
});
}

pub(super) fn fetch_plugin_detail(
&mut self,
app_server: &AppServerSession,
Expand Down Expand Up @@ -249,6 +260,43 @@ impl App {
});
}

pub(super) fn set_hook_enabled(
&mut self,
app_server: &AppServerSession,
key: String,
enabled: bool,
) {
if let Some(queued_enabled) = self.pending_hook_enabled_writes.get_mut(&key) {
*queued_enabled = Some(enabled);
return;
}

self.pending_hook_enabled_writes.insert(key.clone(), None);
self.spawn_hook_enabled_write(app_server, key, enabled);
}

pub(super) fn spawn_hook_enabled_write(
&mut self,
app_server: &AppServerSession,
key: String,
enabled: bool,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let key_for_event = key.clone();
let result = write_hook_enabled(request_handle, key, enabled)
.await
.map(|_| ())
.map_err(|err| format!("Failed to update hook config: {err}"));
app_event_tx.send(AppEvent::HookEnabledSet {
key: key_for_event,
enabled,
result,
});
});
}

pub(super) fn refresh_plugin_mentions(&mut self) {
let config = self.config.clone();
let app_event_tx = self.app_event_tx.clone();
Expand Down Expand Up @@ -541,6 +589,20 @@ pub(super) async fn fetch_plugins_list(
Ok(response)
}

pub(super) async fn fetch_hooks_list(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
) -> Result<HooksListResponse> {
let request_id = RequestId::String(format!("hooks-list-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::HooksList {
request_id,
params: HooksListParams { cwds: vec![cwd] },
})
.await
.wrap_err("hooks/list failed in TUI")
}

const CLI_HIDDEN_PLUGIN_MARKETPLACES: &[&str] = &["openai-bundled"];

pub(super) fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse) {
Expand Down Expand Up @@ -675,6 +737,34 @@ pub(super) async fn write_plugin_enabled(
.wrap_err("config/value/write failed while updating plugin enablement in TUI")
}

pub(super) async fn write_hook_enabled(
request_handle: AppServerRequestHandle,
key: String,
enabled: bool,
) -> Result<ConfigWriteResponse> {
let request_id = RequestId::String(format!("hooks-config-write-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::ConfigBatchWrite {
request_id,
params: ConfigBatchWriteParams {
edits: vec![codex_app_server_protocol::ConfigEdit {
key_path: "hooks.state".to_string(),
value: serde_json::json!({
key: {
"enabled": enabled,
}
}),
merge_strategy: MergeStrategy::Upsert,
}],
file_path: None,
expected_version: None,
reload_user_config: true,
},
})
.await
.wrap_err("config/batchWrite failed while updating hook enablement in TUI")
}

pub(super) fn build_feedback_upload_params(
origin_thread_id: Option<ThreadId>,
rollout_path: Option<PathBuf>,
Expand Down
33 changes: 33 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ impl App {
AppEvent::FetchPluginsList { cwd } => {
self.fetch_plugins_list(app_server, cwd);
}
AppEvent::FetchHooksList { cwd } => {
self.fetch_hooks_list(app_server, cwd);
}
AppEvent::OpenMarketplaceAddPrompt => {
self.chat_widget.open_marketplace_add_prompt();
}
Expand Down Expand Up @@ -426,6 +429,9 @@ impl App {
AppEvent::PluginsLoaded { cwd, result } => {
self.chat_widget.on_plugins_loaded(cwd, result);
}
AppEvent::HooksLoaded { cwd, result } => {
self.chat_widget.on_hooks_loaded(cwd, result);
}
AppEvent::FetchMarketplaceAdd { cwd, source } => {
self.fetch_marketplace_add(app_server, cwd, source);
}
Expand Down Expand Up @@ -1653,6 +1659,33 @@ impl App {
}
}
}
AppEvent::SetHookEnabled { key, enabled } => {
self.set_hook_enabled(app_server, key, enabled);
}
AppEvent::HookEnabledSet {
key,
enabled,
result,
} => {
let queued_enabled = self
.pending_hook_enabled_writes
.get_mut(&key)
.and_then(Option::take);
let should_apply_result = if let Some(queued_enabled) = queued_enabled
&& (result.is_err() || queued_enabled != enabled)
{
self.spawn_hook_enabled_write(app_server, key.clone(), queued_enabled);
false
} else {
true
};
if should_apply_result {
self.pending_hook_enabled_writes.remove(&key);
if let Err(err) = result {
self.chat_widget.add_error_message(err);
}
}
}
AppEvent::OpenPermissionsPopup => {
self.chat_widget.open_permissions_popup();
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub(super) async fn make_test_app() -> App {
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
}
}

Expand Down
2 changes: 2 additions & 0 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3733,6 +3733,7 @@ async fn make_test_app() -> App {
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
}
}

Expand Down Expand Up @@ -3793,6 +3794,7 @@ async fn make_test_app_with_channels() -> (
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
},
rx,
op_rx,
Expand Down
24 changes: 24 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,23 @@ pub(crate) enum AppEvent {
cwd: PathBuf,
},

/// Fetch lifecycle hook inventory for the provided working directory.
FetchHooksList {
cwd: PathBuf,
},

/// Result of fetching plugin marketplace state.
PluginsLoaded {
cwd: PathBuf,
result: Result<PluginListResponse, String>,
},

/// Result of fetching lifecycle hook inventory.
HooksLoaded {
cwd: PathBuf,
result: Result<codex_app_server_protocol::HooksListResponse, String>,
},

/// Open the prompt for adding a marketplace source.
OpenMarketplaceAddPrompt,

Expand Down Expand Up @@ -707,6 +718,19 @@ pub(crate) enum AppEvent {
enabled: bool,
},

/// Enable or disable a hook by stable hook key.
SetHookEnabled {
key: String,
enabled: bool,
},

/// Result of persisting hook enabled state.
HookEnabledSet {
key: String,
enabled: bool,
result: Result<(), String>,
},

/// Notify that the manage skills popup was closed.
ManageSkillsClosed,

Expand Down
Loading
Loading