From d007b0852ad75ce8f365c14f2b8d8dd91ed0eae6 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 15 Jun 2026 17:36:21 -0700 Subject: [PATCH] Preserve hook trust bypass in codex exec threads (#26434) Addresses #26383 and #26452 ## Summary `codex exec --dangerously-bypass-hook-trust` printed the bypass warning, but valid untrusted hooks still did not run. Exec applied the flag to its initial config, then lost it when app-server reloaded config for the new or resumed thread. ## Fix Forward `bypass_hook_trust: true` through the existing thread request config override for both start and resume. The override is omitted when the flag is not enabled, preserving normal trust behavior. ## Testing Added: - A test confirming start and resume preserve the override. - An end-to-end exec test confirming a `SessionStart` hook runs and creates a marker file. --- codex-rs/exec/src/lib.rs | 11 ++++++-- codex-rs/exec/src/lib_tests.rs | 26 ++++++++++++++++++ codex-rs/exec/tests/suite/hooks.rs | 44 ++++++++++++++++++++++++++++++ codex-rs/exec/tests/suite/mod.rs | 1 + 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 codex-rs/exec/tests/suite/hooks.rs diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f151d38ec..f8ac851bf 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -138,6 +138,7 @@ pub use exec_events::TurnStartedEvent; pub use exec_events::Usage; pub use exec_events::WebSearchItem; use serde_json::Value; +use std::collections::HashMap; use std::future::Future; use std::io::IsTerminal; use std::io::Read; @@ -1054,7 +1055,7 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), permissions, - config: None, + config: thread_config_overrides_from_config(config), ephemeral: Some(config.ephemeral), thread_source: Some(ThreadSource::User), ..ThreadStartParams::default() @@ -1079,11 +1080,17 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), permissions, - config: None, + config: thread_config_overrides_from_config(config), ..ThreadResumeParams::default() } } +fn thread_config_overrides_from_config(config: &Config) -> Option> { + config + .bypass_hook_trust + .then(|| HashMap::from([("bypass_hook_trust".to_string(), Value::Bool(true))])) +} + fn permissions_selection_from_config(config: &Config) -> Option { config .permissions diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index de81f60d7..6b27d0111 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -593,6 +593,32 @@ async fn thread_start_params_include_user_thread_source() { ); } +#[tokio::test] +async fn thread_lifecycle_params_preserve_hook_trust_bypass() { + let codex_home = tempdir().expect("create temp codex home"); + let cwd = tempdir().expect("create temp cwd"); + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .harness_overrides(ConfigOverrides { + bypass_hook_trust: Some(true), + ..Default::default() + }) + .fallback_cwd(Some(cwd.path().to_path_buf())) + .build() + .await + .expect("build config with hook trust bypass"); + let expected_config = Some(HashMap::from([( + "bypass_hook_trust".to_string(), + serde_json::Value::Bool(true), + )])); + + let start_params = thread_start_params_from_config(&config); + let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string()); + + assert_eq!(start_params.config, expected_config); + assert_eq!(resume_params.config, expected_config); +} + #[test] fn active_profile_selection_uses_profile_id_only() { let selection = permission_profile_id_from_active_profile(ActivePermissionProfile::new( diff --git a/codex-rs/exec/tests/suite/hooks.rs b/codex-rs/exec/tests/suite/hooks.rs new file mode 100644 index 000000000..52784a3e1 --- /dev/null +++ b/codex-rs/exec/tests/suite/hooks.rs @@ -0,0 +1,44 @@ +#![cfg(not(target_os = "windows"))] +#![allow(clippy::expect_used)] + +use core_test_support::responses; +use core_test_support::test_codex_exec::test_codex_exec; +use serde_json::json; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn exec_hook_trust_bypass_runs_session_start_hook() -> anyhow::Result<()> { + let test = test_codex_exec(); + let marker_path = test.home_path().join("session-start-ran"); + let command = format!("touch {}", marker_path.display()); + std::fs::write( + test.home_path().join("hooks.json"), + serde_json::to_vec_pretty(&json!({ + "hooks": { + "SessionStart": [{ + "hooks": [{ + "type": "command", + "command": command, + }], + }], + }, + }))?, + )?; + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("response_1"), + responses::ev_assistant_message("response_1", "done"), + responses::ev_completed("response_1"), + ]); + responses::mount_sse_once(&server, body).await; + + test.cmd_with_server(&server) + .arg("--skip-git-repo-check") + .arg("--dangerously-bypass-hook-trust") + .arg("run the session start hook") + .assert() + .success(); + + assert!(marker_path.exists(), "session start hook did not run"); + Ok(()) +} diff --git a/codex-rs/exec/tests/suite/mod.rs b/codex-rs/exec/tests/suite/mod.rs index e7452cc17..ceedc890c 100644 --- a/codex-rs/exec/tests/suite/mod.rs +++ b/codex-rs/exec/tests/suite/mod.rs @@ -5,6 +5,7 @@ mod apply_patch; mod approval_policy; mod auth_env; mod ephemeral; +mod hooks; mod mcp_required_exit; mod originator; mod output_schema;