Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion codex-rs/hooks/src/engine/discovery.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;

Expand Down Expand Up @@ -69,6 +70,7 @@ pub(crate) fn discover_handlers(
let mut hook_entries = Vec::new();
let mut warnings = plugin_hook_load_warnings;
let mut display_order = 0_i64;
let mut visited_json_hook_folders = HashSet::new();
let hook_states = hook_states_from_stack(config_layer_stack);
let policy = HookDiscoveryPolicy {
allow_managed_hooks_only: config_layer_stack.is_some_and(|config_layer_stack| {
Expand Down Expand Up @@ -111,7 +113,12 @@ pub(crate) fn discover_handlers(
if !policy.allows(&policy_source) {
continue;
}
let json_hooks = load_hooks_json(layer.hooks_config_folder().as_deref(), &mut warnings);
let json_hooks = match layer.hooks_config_folder() {
Some(config_folder) if visited_json_hook_folders.insert(config_folder.clone()) => {
load_hooks_json(Some(config_folder.as_path()), &mut warnings)
}
_ => None,
};
let toml_hooks = load_toml_hooks_from_layer(layer, &mut warnings);

if let (Some((json_source_path, json_events)), Some((toml_source_path, toml_events))) =
Expand Down
91 changes: 91 additions & 0 deletions codex-rs/hooks/src/engine/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,97 @@ fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
assert_eq!(preview[1].source_path, config_path);
}

#[test]
fn profile_user_layers_load_shared_hooks_json_once() {
let temp = tempdir().expect("create temp dir");
let config_path =
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
let profile_path = AbsolutePathBuf::try_from(temp.path().join("work.config.toml"))
.expect("absolute profile path");
let hooks_json_path =
AbsolutePathBuf::try_from(temp.path().join("hooks.json")).expect("absolute hooks path");
fs::write(
hooks_json_path.as_path(),
r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "python3 /tmp/json-hook.py"
}
]
}
]
}
}"#,
)
.expect("write hooks.json");
let config_layer_stack = ConfigLayerStack::new(
vec![
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: config_path,
profile: None,
},
TomlValue::Table(Default::default()),
),
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: profile_path,
profile: Some("work".to_string()),
},
TomlValue::Table(Default::default()),
),
],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("config layer stack");

let engine = ClaudeHooksEngine::new(
/*enabled*/ true,
/*bypass_hook_trust*/ true,
Some(&config_layer_stack),
Vec::new(),
Vec::new(),
CommandShell {
program: String::new(),
args: Vec::new(),
},
);

assert!(engine.warnings().is_empty());
assert_eq!(engine.handlers.len(), 1);
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
session_id: ThreadId::new(),
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd(),
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
tool_name: "Bash".to_string(),
matcher_aliases: Vec::new(),
tool_use_id: "tool-1".to_string(),
tool_input: serde_json::json!({ "command": "echo hello" }),
});
assert_eq!(preview.len(), 1);
assert_eq!(preview[0].source_path, hooks_json_path);

let listed = crate::list_hooks(crate::HooksConfig {
feature_enabled: true,
bypass_hook_trust: true,
config_layer_stack: Some(config_layer_stack),
..Default::default()
});
assert!(listed.warnings.is_empty());
assert_eq!(listed.hooks.len(), 1);
assert_eq!(listed.hooks[0].source_path, hooks_json_path);
}

#[tokio::test]
async fn plugin_hook_sources_run_with_plugin_env_and_plugin_source() {
let temp = tempdir().expect("create temp dir");
Expand Down
Loading