use std::io::Write;
use std::process::{Command, Stdio};
fn isolated_home() -> std::path::PathBuf {
let home = std::env::temp_dir().join(format!("tokenix-hook-e2e-home-{}", std::process::id()));
std::fs::create_dir_all(&home).expect("isolated TOKENIX_HOME");
home
}
fn run_hook(payload: &str) -> (String, i32) {
let dir = std::env::temp_dir().join(format!(
"tokenix-hook-e2e-{}-{:x}",
std::process::id(),
payload.len()
));
std::fs::create_dir_all(&dir).expect("temp cwd");
let mut child = Command::new(env!("CARGO_BIN_EXE_tokenix"))
.arg("hook")
.current_dir(&dir)
.env("TOKENIX_HOME", isolated_home())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn tokenix hook");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(payload.as_bytes())
.expect("write payload");
let out = child.wait_with_output().expect("hook exit");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
out.status.code().unwrap_or(-1),
)
}
fn claude_bash_payload(command: &str) -> String {
format!(
r#"{{"session_id":"e2e-test","transcript_path":"/tmp/t.jsonl","cwd":"/tmp","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{{"command":{cmd},"description":"e2e"}}}}"#,
cmd = serde_json::to_string(command).unwrap()
)
}
fn updated_command(stdout: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(stdout.trim()).ok()?;
let hso = &v["hookSpecificOutput"];
assert_eq!(
hso["hookEventName"], "PreToolUse",
"rewrite JSON must carry the PreToolUse hookEventName (Claude ignores it otherwise)"
);
hso["updatedInput"]["command"].as_str().map(str::to_string)
}
#[test]
fn claude_bash_filtered_command_is_rewritten_to_tokenix_run() {
let (stdout, code) = run_hook(&claude_bash_payload("terraform plan -out tf.plan"));
assert_eq!(code, 0);
let cmd = updated_command(&stdout).expect("expected a rewrite for a filter-matching command");
assert!(
cmd.contains(" run ") && cmd.contains("terraform plan -out tf.plan"),
"must wrap the original command in `tokenix run`: {cmd}"
);
}
#[test]
fn claude_git_status_is_rewritten_to_short() {
let (stdout, code) = run_hook(&claude_bash_payload("git status"));
assert_eq!(code, 0);
let cmd = updated_command(&stdout).expect("git status must be rewritten");
assert_eq!(cmd, "git status --short");
}
#[test]
fn claude_git_log_is_never_wrapped_in_tokenix_run() {
let (stdout, code) = run_hook(&claude_bash_payload("git log -1"));
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"git log must stay a plain `git` invocation, not opaque tokenix run: {stdout}"
);
}
#[test]
fn claude_compound_command_with_trailing_git_segment_stays_plain() {
let (stdout, code) = run_hook(&claude_bash_payload(
"pwd && git status && git log --oneline -5",
));
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"a compound command ending in `git log` must not be wrapped either: {stdout}"
);
}
#[test]
fn tokenix_disabled_prefix_passes_through() {
let (stdout, code) = run_hook(&claude_bash_payload("TOKENIX_DISABLED=1 terraform plan"));
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"bypassed command must not be rewritten, got: {stdout}"
);
}
#[test]
fn recursive_tokenix_command_passes_through() {
let (stdout, code) = run_hook(&claude_bash_payload("tokenix run \"git status\""));
assert_eq!(code, 0);
assert!(stdout.trim().is_empty(), "no recursion rewrite: {stdout}");
}
#[test]
fn unfiltered_command_passes_through() {
let (stdout, code) = run_hook(&claude_bash_payload("some-unknown-tool-xyz --flag value"));
assert_eq!(code, 0);
assert!(stdout.trim().is_empty(), "no filter → no rewrite: {stdout}");
}
#[test]
fn help_invocation_passes_through() {
let (stdout, code) = run_hook(&claude_bash_payload("terraform plan --help"));
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"help output must never be filtered: {stdout}"
);
}
#[test]
fn malformed_stdin_fails_open() {
let (stdout, code) = run_hook("this is not json {");
assert_eq!(code, 0, "hook must fail open, never block the agent");
assert!(stdout.trim().is_empty());
}
#[test]
fn empty_tool_name_passes_through() {
let (stdout, code) =
run_hook(r#"{"hook_event_name":"PreToolUse","tool_name":"","tool_input":{}}"#);
assert_eq!(code, 0);
assert!(stdout.trim().is_empty());
}
#[test]
fn repeated_successful_command_dedupes_and_stays_retrievable() {
let dir = std::env::temp_dir().join(format!("tokenix-dedup-e2e-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp cwd");
let marker_word = format!("DEDUPFIXTURE{}", std::process::id());
let payload: String = (0..90)
.map(|i| format!("{marker_word}-{i}-filler-text-for-the-token-floor"))
.collect::<Vec<_>>()
.join(" ");
let command = if cfg!(windows) {
format!("echo {payload}")
} else {
format!("echo '{payload}'")
};
let run = |cmd: &str| {
let out = Command::new(env!("CARGO_BIN_EXE_tokenix"))
.args(["run", cmd])
.current_dir(&dir)
.env("TOKENIX_HOME", isolated_home())
.output()
.expect("tokenix run");
String::from_utf8_lossy(&out.stdout).into_owned()
};
let first = run(&command);
assert!(
first.contains(&marker_word) && !first.contains("output identical to"),
"first run must show real output, not a marker: {}",
&first[..first.len().min(200)]
);
let second = run(&command);
if !second.contains("output identical to") {
eprintln!("dedup did not engage in this environment; skipping key check");
let _ = std::fs::remove_dir_all(&dir);
return;
}
assert!(
second.len() < first.len() / 2,
"dedup marker must be far cheaper than the output"
);
let key = second
.split("tokenix retrieve ")
.nth(1)
.and_then(|rest| rest.split(|c: char| !c.is_ascii_alphanumeric()).next())
.expect("marker must carry a retrieve key")
.to_string();
let retrieved = Command::new(env!("CARGO_BIN_EXE_tokenix"))
.args(["retrieve", &key])
.current_dir(&dir)
.env("TOKENIX_HOME", isolated_home())
.output()
.expect("tokenix retrieve");
let body = String::from_utf8_lossy(&retrieved.stdout);
assert!(
retrieved.status.success() && body.contains(&marker_word),
"retrieve must return the original bytes for key {key}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn different_commands_with_identical_output_never_dedupe_e2e() {
let dir = std::env::temp_dir().join(format!(
"tokenix-cross-cmd-dedup-e2e-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).expect("temp cwd");
let home = isolated_home();
let marker_word = format!("CROSSCMDFIXTURE{}", std::process::id());
let payload: String = (0..90)
.map(|i| format!("{marker_word}-{i}-filler-text-for-the-token-floor"))
.collect::<Vec<_>>()
.join(" ");
let (command_a, command_b) = if cfg!(windows) {
(
format!("echo {payload}"),
format!("printf '%s\\n' '{payload}'"),
)
} else {
(
format!("echo '{payload}'"),
format!("printf '%s\\n' '{payload}'"),
)
};
let run = |cmd: &str| {
let out = Command::new(env!("CARGO_BIN_EXE_tokenix"))
.args(["run", cmd])
.current_dir(&dir)
.env("TOKENIX_HOME", &home)
.output()
.expect("tokenix run");
String::from_utf8_lossy(&out.stdout).into_owned()
};
let first = run(&command_a);
assert!(
first.contains(&marker_word) && !first.contains("output identical to"),
"first run (command A) must show real output, not a marker: {}",
&first[..first.len().min(200)]
);
let second = run(&command_b);
assert!(
!second.contains("output identical to"),
"a different command must never be deduped against an unrelated \
command's stash just because their output coincides: {}",
&second[..second.len().min(300)]
);
assert!(
second.contains(&marker_word),
"command B's real output must still be shown: {}",
&second[..second.len().min(200)]
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn uncapped_content_grep_gets_head_limit() {
let payload = r#"{"hook_event_name":"PreToolUse","tool_name":"Grep","tool_input":{"pattern":"foo","output_mode":"content","-C":3}}"#;
let (stdout, code) = run_hook(payload);
assert_eq!(code, 0);
let v: serde_json::Value =
serde_json::from_str(stdout.trim()).unwrap_or_else(|_| panic!("expected JSON: {stdout}"));
let updated = &v["hookSpecificOutput"]["updatedInput"];
assert_eq!(v["hookSpecificOutput"]["hookEventName"], "PreToolUse");
assert!(
updated["head_limit"].is_number(),
"grep must be capped: {stdout}"
);
assert_eq!(updated["pattern"], "foo", "original args preserved");
assert_eq!(updated["-C"], 3);
}
#[test]
fn bounded_grep_passes_through_untouched() {
let payload = r#"{"hook_event_name":"PreToolUse","tool_name":"Grep","tool_input":{"pattern":"foo","output_mode":"content","head_limit":20}}"#;
let (stdout, code) = run_hook(payload);
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"agent-bounded grep must not be rewritten: {stdout}"
);
}
#[test]
fn files_with_matches_grep_passes_through() {
let payload = r#"{"hook_event_name":"PreToolUse","tool_name":"Grep","tool_input":{"pattern":"foo","output_mode":"files_with_matches"}}"#;
let (stdout, code) = run_hook(payload);
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"cheap output mode must not be rewritten: {stdout}"
);
}
#[cfg(windows)]
#[test]
fn powershell_tool_gets_pwsh_shell_rewrite() {
let payload = r#"{"hook_event_name":"PreToolUse","tool_name":"PowerShell","tool_input":{"command":"Get-Content src/main.rs"}}"#;
let (stdout, code) = run_hook(payload);
assert_eq!(code, 0);
let cmd = updated_command(&stdout).expect("PowerShell command must be rewritten");
assert!(
cmd.starts_with("& '") && cmd.contains("run --shell pwsh"),
"must be a native-exe pwsh call: {cmd}"
);
assert!(
cmd.contains("Get-Content src/main.rs"),
"original command preserved: {cmd}"
);
}
#[cfg(windows)]
#[test]
fn powershell_git_log_is_never_wrapped_in_tokenix_run() {
let payload = r#"{"hook_event_name":"PreToolUse","tool_name":"PowerShell","tool_input":{"command":"git log -1"}}"#;
let (stdout, code) = run_hook(payload);
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"git log under PowerShell must stay plain, not wrapped in run --shell pwsh: {stdout}"
);
}
#[cfg(windows)]
#[test]
fn powershell_disabled_env_passes_through() {
let payload = r#"{"hook_event_name":"PreToolUse","tool_name":"PowerShell","tool_input":{"command":"$env:TOKENIX_DISABLED='1'; Get-Content big.log"}}"#;
let (stdout, code) = run_hook(payload);
assert_eq!(code, 0);
assert!(
stdout.trim().is_empty(),
"bypass must skip rewrite: {stdout}"
);
}
fn run_hook_post(payload: &str) -> (String, i32) {
let dir = std::env::temp_dir().join(format!(
"tokenix-hook-post-e2e-{}-{:x}",
std::process::id(),
payload.len()
));
std::fs::create_dir_all(&dir).expect("temp cwd");
let mut child = Command::new(env!("CARGO_BIN_EXE_tokenix"))
.arg("hook-post")
.current_dir(&dir)
.env("TOKENIX_HOME", isolated_home())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn tokenix hook-post");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(payload.as_bytes())
.expect("write payload");
let out = child.wait_with_output().expect("hook-post exit");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
out.status.code().unwrap_or(-1),
)
}
#[test]
fn claude_read_result_secret_is_redacted_via_updated_tool_output() {
let secret = "AKIAIOSFODNN7EXAMPLE"; let payload = format!(
r#"{{"hook_event_name":"PostToolUse","tool_name":"Read","tool_input":{{"file_path":"notes.txt"}},"tool_response":{text}}}"#,
text = serde_json::to_string(&format!("config dump:\naws_key={secret}\n")).unwrap()
);
let (stdout, code) = run_hook_post(&payload);
assert_eq!(code, 0, "hook-post must always exit 0");
let v: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("must emit JSON when a secret was redacted");
assert_eq!(
v["hookSpecificOutput"]["hookEventName"], "PostToolUse",
"must carry the PostToolUse hookEventName"
);
let updated = v["hookSpecificOutput"]["updatedToolOutput"]
.as_str()
.expect("updatedToolOutput must be a string");
assert!(
!updated.contains(secret),
"secret survived into updatedToolOutput: {updated}"
);
assert!(
updated.contains("[REDACTED]"),
"expected a redaction marker: {updated}"
);
}