fix: ignore claude subagent hook reports

fixes #58
This commit is contained in:
Ogulcan Celik 2026-04-27 17:48:08 +03:00
parent 6144aa218e
commit fb4e5a73ff
2 changed files with 104 additions and 3 deletions

View File

@ -5,7 +5,9 @@
set -eu
action="${1:-}"
cat >/dev/null 2>/dev/null || true
hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-claude-hook.XXXXXX")" || exit 0
trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM
cat >"$hook_input_file" 2>/dev/null || true
case "$action" in
working|idle|blocked|release) ;;
@ -17,7 +19,7 @@ esac
[ -n "${HERDR_PANE_ID:-}" ] || exit 0
command -v python3 >/dev/null 2>&1 || exit 0
HERDR_ACTION="$action" python3 - <<'PY'
HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY'
import json
import os
import random
@ -28,10 +30,25 @@ source = "herdr:claude"
action = os.environ.get("HERDR_ACTION", "")
pane_id = os.environ.get("HERDR_PANE_ID")
socket_path = os.environ.get("HERDR_SOCKET_PATH")
hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE")
if not pane_id or not socket_path:
raise SystemExit(0)
hook_input = {}
if hook_input_file:
try:
with open(hook_input_file, encoding="utf-8") as handle:
content = handle.read()
if content.strip():
hook_input = json.loads(content)
except Exception:
hook_input = {}
is_subagent = bool(hook_input.get("agent_id"))
if is_subagent:
raise SystemExit(0)
request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
if action == "release":
request = {

View File

@ -6,7 +6,7 @@ use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@ -286,6 +286,90 @@ fn send_request(socket_path: &Path, json: &str) -> serde_json::Value {
serde_json::from_str(&line).unwrap()
}
fn run_claude_hook(action: &str, hook_input: &str) -> Option<serde_json::Value> {
let base = unique_test_dir();
fs::create_dir_all(&base).unwrap();
let socket_path = base.join("herdr.sock");
let listener = UnixListener::bind(&socket_path).unwrap();
let server = thread::spawn(move || {
listener.set_nonblocking(true).unwrap();
let deadline = Instant::now() + Duration::from_millis(700);
while Instant::now() < deadline {
match listener.accept() {
Ok((mut stream, _)) => {
let mut line = String::new();
let mut reader = BufReader::new(stream.try_clone().unwrap());
reader.read_line(&mut line).unwrap();
stream
.write_all(br#"{"id":"test","result":{"type":"ok"}}"#)
.unwrap();
stream.write_all(b"\n").unwrap();
stream.flush().unwrap();
return Some(line);
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(err) => panic!("accept failed: {err}"),
}
}
None
});
let hook_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/integration/assets/claude/herdr-agent-state.sh");
let mut child = Command::new("bash")
.arg(hook_path)
.arg(action)
.env("HERDR_ENV", "1")
.env("HERDR_SOCKET_PATH", &socket_path)
.env("HERDR_PANE_ID", "p_test")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
stdin.write_all(hook_input.as_bytes()).unwrap();
drop(stdin);
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"hook failed: status={:?} stderr={} stdout={}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
);
let request = server.join().unwrap();
cleanup_test_base(&base);
request.map(|line| serde_json::from_str(&line).unwrap())
}
#[test]
fn claude_hook_suppresses_subagent_reports() {
let subagent_input = r#"{"hook_event_name":"PermissionRequest","agent_id":"agent-abc123","agent_type":"Explore"}"#;
assert!(run_claude_hook("working", subagent_input).is_none());
assert!(run_claude_hook("blocked", subagent_input).is_none());
assert!(run_claude_hook("idle", subagent_input).is_none());
assert!(run_claude_hook("release", subagent_input).is_none());
}
#[test]
fn claude_hook_keeps_parent_agent_type_only_blocked() {
let request = run_claude_hook(
"blocked",
r#"{"hook_event_name":"PermissionRequest","agent_type":"Explore"}"#,
)
.expect("parent blocked should still report blocked");
assert_eq!(request["method"], "pane.report_agent");
assert_eq!(request["params"]["state"], "blocked");
}
#[test]
fn pane_run_sends_one_send_input_request_with_enter_key() {
let base = unique_test_dir();