diff --git a/src/core/utils.rs b/src/core/utils.rs index a3bc84fe0..5794d7c3c 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -259,6 +259,15 @@ pub fn count_tokens(text: &str) -> usize { text.split_whitespace().count() } +/// Returns true when rtk is executing as a Claude Code (or other AI IDE) hook. +/// Set by each hook entry point before any other work so that diagnostic +/// eprintln! calls in filter/trust paths are suppressed — any stderr output +/// during hook execution triggers Claude Code bug #4669 and permanently +/// disables the hook for the session. +pub fn in_hook_mode() -> bool { + std::env::var("RTK_HOOK_MODE").is_ok() +} + /// Detect the package manager used in the current directory. /// Returns "pnpm", "yarn", or "npm" based on lockfile presence. /// @@ -839,6 +848,22 @@ mod tests { assert_eq!(human_bytes(1_099_511_627_776), "1.0 TB"); } + #[test] + fn test_in_hook_mode_unset() { + std::env::remove_var("RTK_HOOK_MODE"); + assert!(!in_hook_mode(), "in_hook_mode() must return false when RTK_HOOK_MODE is unset"); + } + + #[test] + fn test_in_hook_mode_set() { + #[allow(deprecated)] + std::env::set_var("RTK_HOOK_MODE", "1"); + let result = in_hook_mode(); + #[allow(deprecated)] + std::env::remove_var("RTK_HOOK_MODE"); + assert!(result, "in_hook_mode() must return true when RTK_HOOK_MODE=1"); + } + #[test] fn test_count_tokens_basic() { assert_eq!(count_tokens("hello world"), 2); diff --git a/src/hooks/permissions.rs b/src/hooks/permissions.rs index 71536b0e1..768705802 100644 --- a/src/hooks/permissions.rs +++ b/src/hooks/permissions.rs @@ -108,10 +108,12 @@ fn load_permission_rules() -> (Vec, Vec, Vec) { continue; }; let Ok(json) = serde_json::from_str::(&content) else { - eprintln!( - "[rtk] warning: failed to parse permissions from {}", - path.display() - ); + if !crate::core::utils::in_hook_mode() { + eprintln!( + "[rtk] warning: failed to parse permissions from {}", + path.display() + ); + } continue; }; let Some(permissions) = json.get("permissions") else {