aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-19 22:10:15 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-19 22:10:15 +0300
commit104295f492a7376f5d77db12555dbc87db8eabed (patch)
tree04ee105dc12423d55e64ef0dce1b258fa06e8c80
parent2939030cec3d7b190d60c34c156552dfc0c06136 (diff)
fix: report codex permission hooks as blocked
refs #198
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/cli.rs1
-rw-r--r--src/integration/assets/codex/herdr-agent-state.sh2
-rw-r--r--src/integration/mod.rs63
4 files changed, 64 insertions, 3 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 546d213e..4a444440 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -12,6 +12,7 @@
### Fixed
- The Claude Code integration no longer reports successful or failed post-tool hooks as `working`, and installing the updated integration removes Herdr's deprecated post-tool hook entries from existing Claude settings.
+- The Codex integration now reports native `PermissionRequest` hooks as `blocked`, so permission prompts no longer stay pinned as `working` after a tool-use hook.
- Running the internal `herdr client` command from inside Herdr now respects the nested-launch guard, and the command is no longer advertised in root help. (#187)
- The Herdr agent skill now refuses to claim pane ownership unless it is running inside Herdr. (#152)
- Terminal-style docs code blocks now keep their copy button in the top-right corner. (#190)
diff --git a/src/cli.rs b/src/cli.rs
index f57cc625..3e07ab2c 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -2151,6 +2151,7 @@ fn print_integration_help() {
eprintln!(" herdr integration uninstall claude");
eprintln!(" herdr integration uninstall codex");
eprintln!(" herdr integration uninstall opencode");
+ eprintln!(" herdr integration uninstall hermes");
eprintln!(" herdr integration status [--outdated-only]");
}
diff --git a/src/integration/assets/codex/herdr-agent-state.sh b/src/integration/assets/codex/herdr-agent-state.sh
index 0b769d99..fbfa24cb 100644
--- a/src/integration/assets/codex/herdr-agent-state.sh
+++ b/src/integration/assets/codex/herdr-agent-state.sh
@@ -2,7 +2,7 @@
# installed by herdr
# safe to edit. this hook only activates inside herdr-managed panes.
# HERDR_INTEGRATION_ID=codex
-# HERDR_INTEGRATION_VERSION=2
+# HERDR_INTEGRATION_VERSION=3
set -eu
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index 7e418497..c484a8f1 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -20,7 +20,7 @@ const CLAUDE_INTEGRATION_VERSION: u32 = 2;
const CLAUDE_CONFIG_DIR_ENV_VAR: &str = "CLAUDE_CONFIG_DIR";
const CODEX_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const CODEX_HOOK_ASSET: &str = include_str!("assets/codex/herdr-agent-state.sh");
-const CODEX_INTEGRATION_VERSION: u32 = 2;
+const CODEX_INTEGRATION_VERSION: u32 = 3;
const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME";
const OPENCODE_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js";
const OPENCODE_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-agent-state.js");
@@ -608,6 +608,13 @@ pub(crate) fn install_codex() -> io::Result<CodexInstallPaths> {
)?;
ensure_command_hook(
hooks,
+ "PermissionRequest",
+ format!("bash {quoted_hook_path} blocked"),
+ 10,
+ None,
+ )?;
+ ensure_command_hook(
+ hooks,
"Stop",
format!("bash {quoted_hook_path} idle"),
10,
@@ -809,6 +816,11 @@ pub(crate) fn uninstall_codex() -> io::Result<CodexUninstallResult> {
"PreToolUse",
&format!("bash {quoted_hook_path} working"),
)?;
+ updated_hooks |= remove_command_hook(
+ hooks,
+ "PermissionRequest",
+ &format!("bash {quoted_hook_path} blocked"),
+ )?;
updated_hooks |=
remove_command_hook(hooks, "Stop", &format!("bash {quoted_hook_path} idle"))?;
}
@@ -1760,6 +1772,36 @@ mod tests {
}
#[test]
+ fn codex_v2_integration_status_is_outdated() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let home = base.join("home");
+ let codex_dir = home.join(".codex");
+ fs::create_dir_all(&codex_dir).unwrap();
+ let hook_path = codex_dir.join(CODEX_HOOK_INSTALL_NAME);
+ fs::write(
+ &hook_path,
+ "#!/bin/sh\n# HERDR_INTEGRATION_ID=codex\n# HERDR_INTEGRATION_VERSION=2\n",
+ )
+ .unwrap();
+ std::env::set_var("HOME", &home);
+
+ let statuses = installed_integration_statuses();
+ let codex = statuses
+ .iter()
+ .find(|status| status.target == crate::api::schema::IntegrationTarget::Codex)
+ .unwrap();
+
+ assert_eq!(codex.path, hook_path);
+ assert_eq!(codex.installed_version, Some(2));
+ assert_eq!(codex.expected_version, 3);
+ assert_eq!(codex.state, IntegrationStatusKind::Outdated);
+
+ std::env::remove_var("HOME");
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
fn install_codex_writes_hook_and_updates_hooks_and_config() {
let _lock = integration_env_lock();
let base = unique_base();
@@ -1791,6 +1833,12 @@ mod tests {
.as_str()
.unwrap()
.contains(" working"));
+ assert!(
+ hooks["hooks"]["PermissionRequest"][0]["hooks"][0]["command"]
+ .as_str()
+ .unwrap()
+ .contains(" blocked")
+ );
assert!(hooks["hooks"]["Stop"][0]["hooks"][0]["command"]
.as_str()
.unwrap()
@@ -1851,6 +1899,13 @@ mod tests {
1
);
assert_eq!(hooks["hooks"]["PreToolUse"].as_array().unwrap().len(), 1);
+ assert_eq!(
+ hooks["hooks"]["PermissionRequest"]
+ .as_array()
+ .unwrap()
+ .len(),
+ 1
+ );
assert_eq!(hooks["hooks"]["Stop"].as_array().unwrap().len(), 1);
assert_eq!(config.matches("hooks = true").count(), 1);
assert!(!config.contains("codex_hooks"));
@@ -1897,7 +1952,9 @@ mod tests {
fs::write(
codex_dir.join("hooks.json"),
format!(
- r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"UserPromptSubmit":[{{"hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"Stop":[{{"hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}]}}}}"#,
+ r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"UserPromptSubmit":[{{"hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"PreToolUse":[{{"hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"PermissionRequest":[{{"hooks":[{{"type":"command","command":"bash '{}' blocked","timeout":10}}]}}],"Stop":[{{"hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}]}}}}"#,
+ hook_path.display(),
+ hook_path.display(),
hook_path.display(),
hook_path.display(),
hook_path.display(),
@@ -1921,6 +1978,8 @@ mod tests {
assert!(result.updated_hooks);
assert!(!result.hook_path.exists());
assert!(hooks["hooks"].get("SessionStart").is_none());
+ assert!(hooks["hooks"].get("PreToolUse").is_none());
+ assert!(hooks["hooks"].get("PermissionRequest").is_none());
assert!(hooks["hooks"].get("Stop").is_none());
assert_eq!(
hooks["hooks"]["UserPromptSubmit"][0]["hooks"]