Skip to content

Commit cf976df

Browse files
kylehgcclaude
andcommitted
fix(hook): make gemini runner fail open on empty stdin
Amendment to the rtk-ai#2565 cherry-pick: run_gemini parsed input without the empty-input guard the other five runners have, so the new stdin timeout (and plain EOF-without-payload) made it exit nonzero instead of failing open. Guard empty input with the allow response, and cover it with an integration test mirroring the claude one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 04f881a commit cf976df

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

‎src/hooks/hook_cmd.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,16 @@ fn copilot_cli_response_from_decision(
415415
pub fn run_gemini() -> Result<()> {
416416
let input = read_stdin_limited()?;
417417

418-
let json: Value = serde_json::from_str(&input).context("Failed to parse hook input as JSON")?;
418+
// Fail open on empty input (EOF or stdin timeout), like every other
419+
// runner: without this guard the serde parse fails and the hook exits
420+
// nonzero, blocking the tool call it gates.
421+
let input = input.trim();
422+
if input.is_empty() {
423+
print_allow();
424+
return Ok(());
425+
}
426+
427+
let json: Value = serde_json::from_str(input).context("Failed to parse hook input as JSON")?;
419428

420429
let tool_name = json.get("tool_name").and_then(|v| v.as_str()).unwrap_or("");
421430

‎tests/hook_stdin_timeout_test.rs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,43 @@ fn claude_hook_fails_open_when_stdin_stays_open_without_payload() {
7777
thread::sleep(Duration::from_millis(25));
7878
}
7979
}
80+
81+
#[test]
82+
fn gemini_hook_fails_open_when_stdin_stays_open_without_payload() {
83+
let mut child = Command::new(env!("CARGO_BIN_EXE_rtk"))
84+
.args(["hook", "gemini"])
85+
.stdin(Stdio::piped())
86+
.stdout(Stdio::piped())
87+
.stderr(Stdio::piped())
88+
.spawn()
89+
.expect("spawn rtk hook gemini");
90+
91+
let stdin = child.stdin.take().expect("child stdin");
92+
let deadline = Instant::now() + Duration::from_secs(3);
93+
94+
loop {
95+
if child.try_wait().expect("poll child").is_some() {
96+
drop(stdin);
97+
let output = child.wait_with_output().expect("collect hook output");
98+
assert!(
99+
output.status.success(),
100+
"hook should fail open with exit 0, got {:?}, stderr={}",
101+
output.status.code(),
102+
String::from_utf8_lossy(&output.stderr)
103+
);
104+
// Gemini's protocol expects a JSON response; fail-open is allow.
105+
let response: serde_json::Value =
106+
serde_json::from_slice(&output.stdout).expect("gemini JSON response");
107+
assert_eq!(response["decision"], "allow");
108+
return;
109+
}
110+
111+
if Instant::now() >= deadline {
112+
let _ = child.kill();
113+
let _ = child.wait();
114+
panic!("rtk hook gemini blocked on open stdin without payload");
115+
}
116+
117+
thread::sleep(Duration::from_millis(25));
118+
}
119+
}

0 commit comments

Comments
 (0)