mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
add --dangerously-bypass-hook-trust CLI flag (#21768)
# Why Hook trust happens through the TUI in `/hooks` so it can block non-interactive use cases. This flag will allow users that are using codex headlessly to bypass hooks when they want to. # What This adds one invocation-scoped escape hatch. - the CLI flag sets a runtime-only `bypass_hook_trust` override; there is no durable `config.toml` setting - hook discovery still respects normal enablement, so explicitly disabled hooks remain disabled - we show a `--dangerously-bypass-hook-trust is enabled. Enabled hooks may run without review for this invocation.` message on startup so accidental use is visible in both interactive and exec flows This keeps “enabled” and “trusted” as separate concepts in the normal path, while giving CI/E2E callers a stable way to opt into the exceptional path when they already control the hook set.
This commit is contained in:
committed by
GitHub
Unverified
parent
934a40c7d9
commit
392e94e9ea
@@ -525,6 +525,7 @@ impl CatalogRequestProcessor {
|
||||
};
|
||||
let hooks = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: config.features.enabled(Feature::CodexHooks),
|
||||
bypass_hook_trust: config.bypass_hook_trust,
|
||||
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(),
|
||||
|
||||
@@ -1496,6 +1496,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),
|
||||
bypass_hook_trust: shared.bypass_hook_trust.then_some(true),
|
||||
additional_writable_roots: shared.add_dir,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2369,6 +2370,18 @@ mod tests {
|
||||
assert_eq!(interactive.resume_session_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_merges_bypass_hook_trust_flag() {
|
||||
let interactive = finalize_resume_from_args(
|
||||
["codex", "resume", "--dangerously-bypass-hook-trust"].as_ref(),
|
||||
);
|
||||
|
||||
assert!(interactive.bypass_hook_trust);
|
||||
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());
|
||||
|
||||
@@ -7322,6 +7322,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
startup_warnings: Vec::new(),
|
||||
history: History::default(),
|
||||
ephemeral: false,
|
||||
bypass_hook_trust: false,
|
||||
file_opener: UriBasedFileOpener::VsCode,
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
@@ -7769,6 +7770,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
startup_warnings: Vec::new(),
|
||||
history: History::default(),
|
||||
ephemeral: false,
|
||||
bypass_hook_trust: false,
|
||||
file_opener: UriBasedFileOpener::VsCode,
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
@@ -7930,6 +7932,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
startup_warnings: Vec::new(),
|
||||
history: History::default(),
|
||||
ephemeral: false,
|
||||
bypass_hook_trust: false,
|
||||
file_opener: UriBasedFileOpener::VsCode,
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
@@ -8076,6 +8079,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
startup_warnings: Vec::new(),
|
||||
history: History::default(),
|
||||
ephemeral: false,
|
||||
bypass_hook_trust: false,
|
||||
file_opener: UriBasedFileOpener::VsCode,
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
@@ -8973,6 +8977,29 @@ async fn active_profile_is_cleared_when_requirements_force_fallback() -> std::io
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bypass_hook_trust_adds_startup_warning() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.harness_overrides(ConfigOverrides {
|
||||
bypass_hook_trust: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
config.startup_warnings.iter().any(|warning| warning
|
||||
== "`--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation."),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_preserves_split_write_roots() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -668,6 +668,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 bypass_hook_trust: 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,
|
||||
@@ -1886,6 +1891,7 @@ pub struct ConfigOverrides {
|
||||
pub show_raw_agent_reasoning: Option<bool>,
|
||||
pub tools_web_search_request: Option<bool>,
|
||||
pub ephemeral: Option<bool>,
|
||||
pub bypass_hook_trust: Option<bool>,
|
||||
/// Additional directories that should be treated as writable roots for this session.
|
||||
pub additional_writable_roots: Vec<PathBuf>,
|
||||
}
|
||||
@@ -2169,8 +2175,17 @@ impl Config {
|
||||
show_raw_agent_reasoning,
|
||||
tools_web_search_request: override_tools_web_search_request,
|
||||
ephemeral,
|
||||
bypass_hook_trust,
|
||||
additional_writable_roots,
|
||||
} = overrides;
|
||||
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
|
||||
|
||||
if bypass_hook_trust {
|
||||
startup_warnings.push(
|
||||
"`--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if sandbox_mode.is_some() && permission_profile.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -3103,6 +3118,7 @@ impl Config {
|
||||
config_layer_stack,
|
||||
history,
|
||||
ephemeral: ephemeral.unwrap_or_default(),
|
||||
bypass_hook_trust,
|
||||
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
||||
codex_self_exe,
|
||||
codex_linux_sandbox_exe,
|
||||
|
||||
@@ -3362,6 +3362,7 @@ async fn build_hooks_for_config(
|
||||
Hooks::new(HooksConfig {
|
||||
legacy_notify_argv: config.notify.clone(),
|
||||
feature_enabled: config.features.enabled(Feature::CodexHooks),
|
||||
bypass_hook_trust: config.bypass_hook_trust,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
plugin_hook_sources,
|
||||
plugin_hook_load_warnings,
|
||||
|
||||
@@ -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("bypass_hook_trust", |arg| arg.global(true))
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
|
||||
@@ -264,6 +264,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,
|
||||
bypass_hook_trust,
|
||||
cwd,
|
||||
add_dir,
|
||||
} = shared;
|
||||
@@ -422,6 +423,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),
|
||||
bypass_hook_trust: bypass_hook_trust.then_some(true),
|
||||
additional_writable_roots: add_dir,
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ struct HookHandlerSource<'a> {
|
||||
key_source: String,
|
||||
source: HookSource,
|
||||
is_managed: bool,
|
||||
bypass_hook_trust: bool,
|
||||
hook_states: &'a HashMap<String, HookStateToml>,
|
||||
env: HashMap<String, String>,
|
||||
plugin_id: Option<String>,
|
||||
@@ -49,6 +50,7 @@ struct HookHandlerSource<'a> {
|
||||
#[derive(Clone, Copy)]
|
||||
struct HookDiscoveryPolicy {
|
||||
allow_managed_hooks_only: bool,
|
||||
bypass_hook_trust: bool,
|
||||
}
|
||||
|
||||
impl HookDiscoveryPolicy {
|
||||
@@ -61,6 +63,7 @@ pub(crate) fn discover_handlers(
|
||||
config_layer_stack: Option<&ConfigLayerStack>,
|
||||
plugin_hook_sources: Vec<PluginHookSource>,
|
||||
plugin_hook_load_warnings: Vec<String>,
|
||||
bypass_hook_trust: bool,
|
||||
) -> DiscoveryResult {
|
||||
let mut handlers = Vec::new();
|
||||
let mut hook_entries = Vec::new();
|
||||
@@ -75,6 +78,7 @@ pub(crate) fn discover_handlers(
|
||||
.as_ref()
|
||||
.is_some_and(|requirement| requirement.value)
|
||||
}),
|
||||
bypass_hook_trust,
|
||||
};
|
||||
|
||||
if let Some(config_layer_stack) = config_layer_stack {
|
||||
@@ -99,6 +103,7 @@ pub(crate) fn discover_handlers(
|
||||
key_source: policy_path.display().to_string(),
|
||||
source: hook_source,
|
||||
is_managed,
|
||||
bypass_hook_trust: false,
|
||||
hook_states: &hook_states,
|
||||
env: HashMap::new(),
|
||||
plugin_id: None,
|
||||
@@ -132,6 +137,7 @@ pub(crate) fn discover_handlers(
|
||||
key_source: source_path.display().to_string(),
|
||||
source: hook_source,
|
||||
is_managed,
|
||||
bypass_hook_trust: policy.bypass_hook_trust,
|
||||
hook_states: &hook_states,
|
||||
env: HashMap::new(),
|
||||
plugin_id: None,
|
||||
@@ -183,6 +189,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,
|
||||
bypass_hook_trust: false,
|
||||
hook_states,
|
||||
env: HashMap::new(),
|
||||
plugin_id: None,
|
||||
@@ -233,6 +240,7 @@ fn append_plugin_hook_sources(
|
||||
),
|
||||
source: HookSource::Plugin,
|
||||
is_managed: false,
|
||||
bypass_hook_trust: policy.bypass_hook_trust,
|
||||
hook_states,
|
||||
env,
|
||||
plugin_id: Some(plugin_id),
|
||||
@@ -485,10 +493,11 @@ fn append_matcher_groups(
|
||||
trust_status,
|
||||
});
|
||||
if enabled
|
||||
&& matches!(
|
||||
trust_status,
|
||||
HookTrustStatus::Managed | HookTrustStatus::Trusted
|
||||
)
|
||||
&& (source.bypass_hook_trust
|
||||
|| matches!(
|
||||
trust_status,
|
||||
HookTrustStatus::Managed | HookTrustStatus::Trusted
|
||||
))
|
||||
{
|
||||
handlers.push(ConfiguredHandler {
|
||||
event_name,
|
||||
@@ -620,6 +629,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()
|
||||
@@ -638,6 +648,24 @@ mod tests {
|
||||
key_source: path.display().to_string(),
|
||||
source: hook_source(),
|
||||
is_managed: true,
|
||||
bypass_hook_trust: 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>,
|
||||
bypass_hook_trust: bool,
|
||||
) -> super::HookHandlerSource<'a> {
|
||||
super::HookHandlerSource {
|
||||
path,
|
||||
key_source: path.display().to_string(),
|
||||
source: HookSource::User,
|
||||
is_managed: false,
|
||||
bypass_hook_trust,
|
||||
hook_states,
|
||||
env: std::collections::HashMap::new(),
|
||||
plugin_id: None,
|
||||
@@ -727,6 +755,72 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bypass_hook_trust_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,
|
||||
/*bypass_hook_trust*/ 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 bypass_hook_trust_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,
|
||||
/*bypass_hook_trust*/ 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();
|
||||
|
||||
@@ -106,6 +106,7 @@ pub(crate) struct ClaudeHooksEngine {
|
||||
impl ClaudeHooksEngine {
|
||||
pub(crate) fn new(
|
||||
enabled: bool,
|
||||
bypass_hook_trust: 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,
|
||||
bypass_hook_trust,
|
||||
);
|
||||
Self {
|
||||
handlers: discovered.handlers,
|
||||
|
||||
@@ -196,6 +196,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -211,6 +212,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,
|
||||
bypass_hook_trust: false,
|
||||
config_layer_stack: Some(config_layer_stack.clone()),
|
||||
plugin_hook_sources: Vec::new(),
|
||||
plugin_hook_load_warnings: Vec::new(),
|
||||
@@ -295,6 +297,7 @@ async fn requirements_managed_hooks_execute_windows_command_override() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -372,6 +375,7 @@ fn unknown_requirement_source_hooks_stay_managed() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -383,8 +387,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(),
|
||||
/*bypass_hook_trust*/ 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);
|
||||
@@ -446,6 +454,7 @@ fn user_disablement_filters_non_managed_hooks_but_not_managed_hooks() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -457,8 +466,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(),
|
||||
/*bypass_hook_trust*/ 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);
|
||||
@@ -503,6 +516,7 @@ fn user_disablement_does_not_filter_managed_layer_hooks() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -517,8 +531,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(),
|
||||
/*bypass_hook_trust*/ 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);
|
||||
@@ -586,6 +604,7 @@ fn trusted_plugin_hook_stack(
|
||||
/*config_layer_stack*/ None,
|
||||
plugin_hook_sources.to_vec(),
|
||||
Vec::new(),
|
||||
/*bypass_hook_trust*/ false,
|
||||
);
|
||||
let state = discovered
|
||||
.hook_entries
|
||||
@@ -655,6 +674,7 @@ fn requirements_managed_hooks_load_when_managed_dir_is_missing() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -706,6 +726,7 @@ fn allow_managed_hooks_only_false_keeps_unmanaged_hooks() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -717,8 +738,12 @@ fn allow_managed_hooks_only_false_keeps_unmanaged_hooks() {
|
||||
|
||||
assert!(engine.warnings().is_empty());
|
||||
assert!(engine.handlers.is_empty());
|
||||
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(),
|
||||
/*bypass_hook_trust*/ false,
|
||||
);
|
||||
assert_eq!(discovered.hook_entries.len(), 1);
|
||||
assert!(!discovered.hook_entries[0].is_managed);
|
||||
assert_eq!(
|
||||
@@ -752,6 +777,7 @@ fn allow_managed_hooks_only_in_config_toml_does_not_enable_policy() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -763,8 +789,12 @@ fn allow_managed_hooks_only_in_config_toml_does_not_enable_policy() {
|
||||
|
||||
assert!(engine.warnings().is_empty());
|
||||
assert!(engine.handlers.is_empty());
|
||||
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(),
|
||||
/*bypass_hook_trust*/ false,
|
||||
);
|
||||
assert_eq!(discovered.hook_entries.len(), 1);
|
||||
assert!(!discovered.hook_entries[0].is_managed);
|
||||
assert_eq!(
|
||||
@@ -814,6 +844,7 @@ fn allow_managed_hooks_only_skips_unmanaged_json_and_toml_hooks() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -852,6 +883,7 @@ fn allow_managed_hooks_only_skips_unmanaged_plugin_hooks() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
plugin_hook_sources,
|
||||
Vec::new(),
|
||||
@@ -923,6 +955,7 @@ fn allow_managed_hooks_only_keeps_managed_requirement_and_config_layer_hooks() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -947,8 +980,12 @@ fn allow_managed_hooks_only_keeps_managed_requirement_and_config_layer_hooks() {
|
||||
"python3 /tmp/legacy-mdm-hook.py",
|
||||
]
|
||||
);
|
||||
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(),
|
||||
/*bypass_hook_trust*/ false,
|
||||
);
|
||||
assert!(discovered.hook_entries.iter().all(|entry| entry.is_managed));
|
||||
}
|
||||
|
||||
@@ -1028,6 +1065,7 @@ fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@@ -1119,6 +1157,7 @@ print(json.dumps({
|
||||
);
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
plugin_hook_sources.clone(),
|
||||
Vec::new(),
|
||||
@@ -1146,6 +1185,7 @@ print(json.dumps({
|
||||
let listed = crate::list_hooks(crate::HooksConfig {
|
||||
legacy_notify_argv: None,
|
||||
feature_enabled: true,
|
||||
bypass_hook_trust: false,
|
||||
config_layer_stack: None,
|
||||
plugin_hook_sources,
|
||||
plugin_hook_load_warnings: Vec::new(),
|
||||
@@ -1229,6 +1269,7 @@ fn plugin_hook_sources_expand_plugin_placeholders() {
|
||||
);
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
Some(&config_layer_stack),
|
||||
plugin_hook_sources,
|
||||
Vec::new(),
|
||||
@@ -1272,6 +1313,7 @@ fn plugin_hook_sources_expand_plugin_placeholders() {
|
||||
fn plugin_hook_load_warnings_are_startup_warnings() {
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
/*bypass_hook_trust*/ false,
|
||||
/*config_layer_stack*/ None,
|
||||
Vec::new(),
|
||||
vec!["failed plugin hook".to_string()],
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::types::HookResponse;
|
||||
pub struct HooksConfig {
|
||||
pub legacy_notify_argv: Option<Vec<String>>,
|
||||
pub feature_enabled: bool,
|
||||
pub bypass_hook_trust: bool,
|
||||
pub config_layer_stack: Option<ConfigLayerStack>,
|
||||
pub plugin_hook_sources: Vec<PluginHookSource>,
|
||||
pub plugin_hook_load_warnings: Vec<String>,
|
||||
@@ -65,6 +66,7 @@ impl Hooks {
|
||||
.collect();
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
config.feature_enabled,
|
||||
config.bypass_hook_trust,
|
||||
config.config_layer_stack.as_ref(),
|
||||
config.plugin_hook_sources,
|
||||
config.plugin_hook_load_warnings,
|
||||
@@ -212,6 +214,7 @@ pub fn list_hooks(config: HooksConfig) -> HookListOutcome {
|
||||
config.config_layer_stack.as_ref(),
|
||||
config.plugin_hook_sources,
|
||||
config.plugin_hook_load_warnings,
|
||||
config.bypass_hook_trust,
|
||||
);
|
||||
HookListOutcome {
|
||||
hooks: discovered.hook_entries,
|
||||
|
||||
@@ -163,6 +163,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
let mut config = Config {
|
||||
config_layer_stack: ConfigLayerStack::default(),
|
||||
startup_warnings: Vec::new(),
|
||||
bypass_hook_trust: false,
|
||||
model,
|
||||
service_tier: None,
|
||||
review_model: None,
|
||||
|
||||
@@ -304,6 +304,18 @@ async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread(
|
||||
assert!(app.transcript_cells.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bypass_hook_trust_startup_warning_snapshot() {
|
||||
let rendered = lines_to_single_string(
|
||||
&history_cell::new_warning_event(
|
||||
"`--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation."
|
||||
.to_string(),
|
||||
)
|
||||
.display_lines(/*width*/ 80),
|
||||
);
|
||||
|
||||
assert_app_snapshot!("bypass_hook_trust_startup_warning", rendered);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn enqueue_primary_thread_session_replays_buffered_approval_after_attach() -> Result<()> {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
@@ -928,6 +928,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),
|
||||
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
|
||||
additional_writable_roots: additional_dirs,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/app/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
⚠ `--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without
|
||||
review for this invocation.
|
||||
@@ -47,6 +47,11 @@ pub struct SharedCliOptions {
|
||||
)]
|
||||
pub dangerously_bypass_approvals_and_sandbox: bool,
|
||||
|
||||
/// Run enabled hooks without requiring persisted hook trust for this invocation.
|
||||
/// DANGEROUS. Intended only for automation that already vets hook sources.
|
||||
#[arg(long = "dangerously-bypass-hook-trust", default_value_t = false)]
|
||||
pub bypass_hook_trust: 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 +73,7 @@ impl SharedCliOptions {
|
||||
config_profile,
|
||||
sandbox_mode,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
bypass_hook_trust,
|
||||
cwd,
|
||||
add_dir,
|
||||
} = self;
|
||||
@@ -79,6 +85,7 @@ impl SharedCliOptions {
|
||||
config_profile: root_config_profile,
|
||||
sandbox_mode: root_sandbox_mode,
|
||||
dangerously_bypass_approvals_and_sandbox: root_dangerously_bypass_approvals_and_sandbox,
|
||||
bypass_hook_trust: root_bypass_hook_trust,
|
||||
cwd: root_cwd,
|
||||
add_dir: root_add_dir,
|
||||
} = root;
|
||||
@@ -102,6 +109,9 @@ impl SharedCliOptions {
|
||||
*dangerously_bypass_approvals_and_sandbox =
|
||||
*root_dangerously_bypass_approvals_and_sandbox;
|
||||
}
|
||||
if !*bypass_hook_trust {
|
||||
*bypass_hook_trust = *root_bypass_hook_trust;
|
||||
}
|
||||
if cwd.is_none() {
|
||||
cwd.clone_from(root_cwd);
|
||||
}
|
||||
@@ -128,6 +138,7 @@ impl SharedCliOptions {
|
||||
config_profile,
|
||||
sandbox_mode,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
bypass_hook_trust,
|
||||
cwd,
|
||||
add_dir,
|
||||
} = subcommand;
|
||||
@@ -149,6 +160,9 @@ impl SharedCliOptions {
|
||||
self.dangerously_bypass_approvals_and_sandbox =
|
||||
dangerously_bypass_approvals_and_sandbox;
|
||||
}
|
||||
if bypass_hook_trust {
|
||||
self.bypass_hook_trust = true;
|
||||
}
|
||||
if let Some(cwd) = cwd {
|
||||
self.cwd = Some(cwd);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user