add trust hooks CLI flag

This commit is contained in:
Abhinav Vedmala
2026-05-08 11:53:06 -04:00
parent 163eac9306
commit 9f82e113cd
13 changed files with 166 additions and 10 deletions

View File

@@ -566,6 +566,7 @@ impl CatalogRequestProcessor {
};
let hooks = codex_hooks::list_hooks(codex_hooks::HooksConfig {
feature_enabled: config.features.enabled(Feature::CodexHooks),
trust_hooks: config.trust_hooks,
config_layer_stack: Some(config.config_layer_stack),
plugin_hook_sources: plugin_outcome.effective_plugin_hook_sources(),
plugin_hook_load_warnings: plugin_outcome.effective_plugin_hook_warnings(),

View File

@@ -1421,6 +1421,7 @@ async fn run_debug_prompt_input_command(
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
show_raw_agent_reasoning: shared.oss.then_some(true),
ephemeral: Some(true),
trust_hooks: shared.trust_hooks.then_some(true),
additional_writable_roots: shared.add_dir,
..Default::default()
};
@@ -2254,6 +2255,16 @@ mod tests {
assert_eq!(interactive.resume_session_id, None);
}
#[test]
fn resume_merges_trust_hooks_flag() {
let interactive = finalize_resume_from_args(["codex", "resume", "--trust-hooks"].as_ref());
assert!(interactive.trust_hooks);
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
}
#[test]
fn fork_picker_logic_none_and_not_last() {
let interactive = finalize_fork_from_args(["codex", "fork"].as_ref());

View File

@@ -6992,6 +6992,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
trust_hooks: false,
file_opener: UriBasedFileOpener::VsCode,
codex_self_exe: None,
codex_linux_sandbox_exe: None,
@@ -7251,6 +7252,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
trust_hooks: false,
file_opener: UriBasedFileOpener::VsCode,
codex_self_exe: None,
codex_linux_sandbox_exe: None,
@@ -7409,6 +7411,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
trust_hooks: false,
file_opener: UriBasedFileOpener::VsCode,
codex_self_exe: None,
codex_linux_sandbox_exe: None,
@@ -7552,6 +7555,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
trust_hooks: false,
file_opener: UriBasedFileOpener::VsCode,
codex_self_exe: None,
codex_linux_sandbox_exe: None,

View File

@@ -660,6 +660,11 @@ pub struct Config {
/// When true, session is not persisted on disk. Default to `false`
pub ephemeral: bool,
/// Whether enabled hooks should run without requiring persisted hook trust for this session.
///
/// This is a runtime-only knob populated from invocation overrides, not from config files.
pub trust_hooks: bool,
/// Optional URI-based file opener. If set, citations to files in the model
/// output will be hyperlinked using the specified URI scheme.
pub file_opener: UriBasedFileOpener,
@@ -1884,6 +1889,7 @@ pub struct ConfigOverrides {
pub show_raw_agent_reasoning: Option<bool>,
pub tools_web_search_request: Option<bool>,
pub ephemeral: Option<bool>,
pub trust_hooks: Option<bool>,
/// Additional directories that should be treated as writable roots for this session.
pub additional_writable_roots: Vec<PathBuf>,
}
@@ -2152,6 +2158,7 @@ impl Config {
show_raw_agent_reasoning,
tools_web_search_request: override_tools_web_search_request,
ephemeral,
trust_hooks,
additional_writable_roots,
} = overrides;
@@ -3072,6 +3079,7 @@ impl Config {
config_layer_stack,
history,
ephemeral: ephemeral.unwrap_or_default(),
trust_hooks: trust_hooks.unwrap_or_default(),
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
codex_self_exe,
codex_linux_sandbox_exe,

View File

@@ -3359,6 +3359,7 @@ async fn build_hooks_for_config(
Hooks::new(HooksConfig {
legacy_notify_argv: config.notify.clone(),
feature_enabled: config.features.enabled(Feature::CodexHooks),
trust_hooks: config.trust_hooks,
config_layer_stack: Some(config.config_layer_stack.clone()),
plugin_hook_sources,
plugin_hook_load_warnings,

View File

@@ -155,6 +155,7 @@ fn mark_exec_global_args(cmd: clap::Command) -> clap::Command {
.mut_arg("dangerously_bypass_approvals_and_sandbox", |arg| {
arg.global(true)
})
.mut_arg("trust_hooks", |arg| arg.global(true))
}
#[derive(Debug, clap::Subcommand)]

View File

@@ -265,6 +265,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
config_profile,
sandbox_mode: sandbox_mode_cli_arg,
dangerously_bypass_approvals_and_sandbox,
trust_hooks,
cwd,
add_dir,
} = shared;
@@ -423,6 +424,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
show_raw_agent_reasoning: oss.then_some(true),
tools_web_search_request: None,
ephemeral: ephemeral.then_some(true),
trust_hooks: trust_hooks.then_some(true),
additional_writable_roots: add_dir,
};

View File

@@ -41,6 +41,7 @@ struct HookHandlerSource<'a> {
key_source: String,
source: HookSource,
is_managed: bool,
trust_hooks: bool,
hook_states: &'a HashMap<String, HookStateToml>,
env: HashMap<String, String>,
plugin_id: Option<String>,
@@ -50,6 +51,7 @@ pub(crate) fn discover_handlers(
config_layer_stack: Option<&ConfigLayerStack>,
plugin_hook_sources: Vec<PluginHookSource>,
plugin_hook_load_warnings: Vec<String>,
trust_hooks: bool,
) -> DiscoveryResult {
let mut handlers = Vec::new();
let mut hook_entries = Vec::new();
@@ -98,6 +100,7 @@ pub(crate) fn discover_handlers(
key_source: source_path.display().to_string(),
source: hook_source,
is_managed,
trust_hooks,
hook_states: &hook_states,
env: HashMap::new(),
plugin_id: None,
@@ -115,6 +118,7 @@ pub(crate) fn discover_handlers(
&mut display_order,
plugin_hook_sources,
&hook_states,
trust_hooks,
);
DiscoveryResult {
@@ -150,6 +154,7 @@ fn append_managed_requirement_handlers(
key_source: source_path.display().to_string(),
source: hook_source_for_requirement_source(managed_hooks.source.as_ref()),
is_managed: true,
trust_hooks: false,
hook_states,
env: HashMap::new(),
plugin_id: None,
@@ -165,6 +170,7 @@ fn append_plugin_hook_sources(
display_order: &mut i64,
plugin_hook_sources: Vec<PluginHookSource>,
hook_states: &HashMap<String, HookStateToml>,
trust_hooks: bool,
) {
for source in plugin_hook_sources {
let PluginHookSource {
@@ -198,6 +204,7 @@ fn append_plugin_hook_sources(
),
source: HookSource::Plugin,
is_managed: false,
trust_hooks,
hook_states,
env,
plugin_id: Some(plugin_id),
@@ -444,10 +451,11 @@ fn append_matcher_groups(
trust_status,
});
if enabled
&& matches!(
trust_status,
HookTrustStatus::Managed | HookTrustStatus::Trusted
)
&& (source.trust_hooks
|| matches!(
trust_status,
HookTrustStatus::Managed | HookTrustStatus::Trusted
))
{
handlers.push(ConfiguredHandler {
event_name,
@@ -579,6 +587,7 @@ mod tests {
use codex_config::HookStateToml;
use codex_config::MatcherGroup;
use codex_config::TomlValue;
use codex_protocol::protocol::HookTrustStatus;
fn source_path() -> AbsolutePathBuf {
test_path_buf("/tmp/hooks.json").abs()
@@ -597,6 +606,24 @@ mod tests {
key_source: path.display().to_string(),
source: hook_source(),
is_managed: true,
trust_hooks: false,
hook_states,
env: std::collections::HashMap::new(),
plugin_id: None,
}
}
fn unmanaged_hook_handler_source<'a>(
path: &'a AbsolutePathBuf,
hook_states: &'a std::collections::HashMap<String, HookStateToml>,
trust_hooks: bool,
) -> super::HookHandlerSource<'a> {
super::HookHandlerSource {
path,
key_source: path.display().to_string(),
source: HookSource::User,
is_managed: false,
trust_hooks,
hook_states,
env: std::collections::HashMap::new(),
plugin_id: None,
@@ -685,6 +712,64 @@ mod tests {
);
}
#[test]
fn trust_hooks_allows_enabled_untrusted_handlers() {
let mut handlers = Vec::new();
let mut hook_entries = Vec::new();
let mut warnings = Vec::new();
let mut display_order = 0;
let source_path = source_path();
let hook_states = std::collections::HashMap::new();
append_matcher_groups(
&mut handlers,
&mut hook_entries,
&mut warnings,
&mut display_order,
&unmanaged_hook_handler_source(&source_path, &hook_states, /*trust_hooks*/ true),
HookEventName::PreToolUse,
vec![command_group(Some("Bash"))],
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(handlers.len(), 1);
assert_eq!(hook_entries.len(), 1);
assert_eq!(hook_entries[0].trust_status, HookTrustStatus::Untrusted);
assert_eq!(hook_entries[0].enabled, true);
}
#[test]
fn trust_hooks_respects_disabled_handlers() {
let mut handlers = Vec::new();
let mut hook_entries = Vec::new();
let mut warnings = Vec::new();
let mut display_order = 0;
let source_path = source_path();
let hook_states = std::collections::HashMap::from([(
format!("{}:pre_tool_use:0:0", source_path.display()),
HookStateToml {
enabled: Some(false),
trusted_hash: None,
},
)]);
append_matcher_groups(
&mut handlers,
&mut hook_entries,
&mut warnings,
&mut display_order,
&unmanaged_hook_handler_source(&source_path, &hook_states, /*trust_hooks*/ true),
HookEventName::PreToolUse,
vec![command_group(Some("Bash"))],
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(handlers, Vec::<ConfiguredHandler>::new());
assert_eq!(hook_entries.len(), 1);
assert_eq!(hook_entries[0].trust_status, HookTrustStatus::Untrusted);
assert_eq!(hook_entries[0].enabled, false);
}
#[test]
fn pre_tool_use_treats_star_matcher_as_match_all() {
let mut handlers = Vec::new();

View File

@@ -106,6 +106,7 @@ pub(crate) struct ClaudeHooksEngine {
impl ClaudeHooksEngine {
pub(crate) fn new(
enabled: bool,
trust_hooks: bool,
config_layer_stack: Option<&ConfigLayerStack>,
plugin_hook_sources: Vec<PluginHookSource>,
plugin_hook_load_warnings: Vec<String>,
@@ -125,6 +126,7 @@ impl ClaudeHooksEngine {
config_layer_stack,
plugin_hook_sources,
plugin_hook_load_warnings,
trust_hooks,
);
Self {
handlers: discovered.handlers,

View File

@@ -111,6 +111,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
@@ -126,6 +127,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
let listed = crate::list_hooks(crate::HooksConfig {
legacy_notify_argv: None,
feature_enabled: true,
trust_hooks: false,
config_layer_stack: Some(config_layer_stack.clone()),
plugin_hook_sources: Vec::new(),
plugin_hook_load_warnings: Vec::new(),
@@ -208,6 +210,7 @@ fn unknown_requirement_source_hooks_stay_managed() {
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
@@ -219,8 +222,12 @@ fn unknown_requirement_source_hooks_stay_managed() {
assert_eq!(engine.handlers.len(), 1);
assert_eq!(engine.handlers[0].source, HookSource::Unknown);
let discovered =
super::discovery::discover_handlers(Some(&config_layer_stack), Vec::new(), Vec::new());
let discovered = super::discovery::discover_handlers(
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
/*trust_hooks*/ false,
);
assert_eq!(discovered.hook_entries.len(), 1);
assert_eq!(discovered.hook_entries[0].source, HookSource::Unknown);
assert_eq!(discovered.hook_entries[0].enabled, true);
@@ -281,6 +288,7 @@ fn user_disablement_filters_non_managed_hooks_but_not_managed_hooks() {
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
@@ -292,8 +300,12 @@ fn user_disablement_filters_non_managed_hooks_but_not_managed_hooks() {
assert_eq!(engine.handlers.len(), 1);
assert_eq!(engine.handlers[0].source, HookSource::CloudRequirements);
let discovered =
super::discovery::discover_handlers(Some(&config_layer_stack), Vec::new(), Vec::new());
let discovered = super::discovery::discover_handlers(
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
/*trust_hooks*/ false,
);
assert_eq!(discovered.hook_entries.len(), 2);
assert_eq!(discovered.hook_entries[0].key, managed_disabled_key);
assert_eq!(discovered.hook_entries[0].enabled, true);
@@ -338,6 +350,7 @@ fn user_disablement_does_not_filter_managed_layer_hooks() {
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
@@ -352,8 +365,12 @@ fn user_disablement_does_not_filter_managed_layer_hooks() {
engine.handlers[0].source,
HookSource::LegacyManagedConfigFile
);
let discovered =
super::discovery::discover_handlers(Some(&config_layer_stack), Vec::new(), Vec::new());
let discovered = super::discovery::discover_handlers(
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
/*trust_hooks*/ false,
);
assert_eq!(discovered.hook_entries.len(), 1);
assert_eq!(discovered.hook_entries[0].key, managed_key);
assert_eq!(discovered.hook_entries[0].enabled, true);
@@ -421,6 +438,7 @@ fn trusted_plugin_hook_stack(
/*config_layer_stack*/ None,
plugin_hook_sources.to_vec(),
Vec::new(),
/*trust_hooks*/ false,
);
let state = discovered
.hook_entries
@@ -489,6 +507,7 @@ fn requirements_managed_hooks_warn_when_managed_dir_is_missing() {
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
@@ -598,6 +617,7 @@ fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
@@ -688,6 +708,7 @@ print(json.dumps({
);
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
plugin_hook_sources.clone(),
Vec::new(),
@@ -715,6 +736,7 @@ print(json.dumps({
let listed = crate::list_hooks(crate::HooksConfig {
legacy_notify_argv: None,
feature_enabled: true,
trust_hooks: false,
config_layer_stack: None,
plugin_hook_sources,
plugin_hook_load_warnings: Vec::new(),
@@ -796,6 +818,7 @@ fn plugin_hook_sources_expand_plugin_placeholders() {
);
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
Some(&config_layer_stack),
plugin_hook_sources,
Vec::new(),
@@ -839,6 +862,7 @@ fn plugin_hook_sources_expand_plugin_placeholders() {
fn plugin_hook_load_warnings_are_startup_warnings() {
let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*trust_hooks*/ false,
/*config_layer_stack*/ None,
Vec::new(),
vec!["failed plugin hook".to_string()],

View File

@@ -30,6 +30,7 @@ use crate::types::HookResponse;
pub struct HooksConfig {
pub legacy_notify_argv: Option<Vec<String>>,
pub feature_enabled: bool,
pub trust_hooks: bool,
pub config_layer_stack: Option<ConfigLayerStack>,
pub plugin_hook_sources: Vec<PluginHookSource>,
pub plugin_hook_load_warnings: Vec<String>,
@@ -66,6 +67,7 @@ impl Hooks {
.collect();
let engine = ClaudeHooksEngine::new(
config.feature_enabled,
config.trust_hooks,
config.config_layer_stack.as_ref(),
config.plugin_hook_sources,
config.plugin_hook_load_warnings,
@@ -215,6 +217,7 @@ pub fn list_hooks(config: HooksConfig) -> HookListOutcome {
config.config_layer_stack.as_ref(),
config.plugin_hook_sources,
config.plugin_hook_load_warnings,
config.trust_hooks,
);
HookListOutcome {
hooks: discovered.hook_entries,

View File

@@ -865,6 +865,7 @@ pub async fn run_main(
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
show_raw_agent_reasoning: cli.oss.then_some(true),
trust_hooks: cli.trust_hooks.then_some(true),
additional_writable_roots: additional_dirs,
..Default::default()
};

View File

@@ -47,6 +47,10 @@ pub struct SharedCliOptions {
)]
pub dangerously_bypass_approvals_and_sandbox: bool,
/// Run enabled hooks without requiring persisted hook trust for this invocation.
#[arg(long = "trust-hooks", default_value_t = false)]
pub trust_hooks: bool,
/// Tell the agent to use the specified directory as its working root.
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
@@ -68,6 +72,7 @@ impl SharedCliOptions {
config_profile,
sandbox_mode,
dangerously_bypass_approvals_and_sandbox,
trust_hooks,
cwd,
add_dir,
} = self;
@@ -79,6 +84,7 @@ impl SharedCliOptions {
config_profile: root_config_profile,
sandbox_mode: root_sandbox_mode,
dangerously_bypass_approvals_and_sandbox: root_dangerously_bypass_approvals_and_sandbox,
trust_hooks: root_trust_hooks,
cwd: root_cwd,
add_dir: root_add_dir,
} = root;
@@ -102,6 +108,9 @@ impl SharedCliOptions {
*dangerously_bypass_approvals_and_sandbox =
*root_dangerously_bypass_approvals_and_sandbox;
}
if !*trust_hooks {
*trust_hooks = *root_trust_hooks;
}
if cwd.is_none() {
cwd.clone_from(root_cwd);
}
@@ -128,6 +137,7 @@ impl SharedCliOptions {
config_profile,
sandbox_mode,
dangerously_bypass_approvals_and_sandbox,
trust_hooks,
cwd,
add_dir,
} = subcommand;
@@ -149,6 +159,9 @@ impl SharedCliOptions {
self.dangerously_bypass_approvals_and_sandbox =
dangerously_bypass_approvals_and_sandbox;
}
if trust_hooks {
self.trust_hooks = true;
}
if let Some(cwd) = cwd {
self.cwd = Some(cwd);
}