use trusty_agents_common::compress::has_filter_for;
use trusty_agents_common::compress::tool_output::is_source_file_read;
use std::path::{Path, PathBuf};
use trusty_mpm::core::project_aliases::worktree_root;
const ORCHESTRATOR_EXCLUSIONS: &[&str] = &[
"make",
"sam",
"rake",
"gradle",
"gradlew",
"mvn",
"ant",
"cdk",
"terraform",
];
pub(crate) fn rewrite_bash_command_for_compression(command: &str) -> Option<String> {
let trimmed = command.trim();
if trimmed.is_empty() {
return None;
}
if is_orchestrator_command(trimmed) {
return None;
}
if has_unsafe_pipe_composition(trimmed) {
return None;
}
if cannot_be_brace_wrapped(trimmed) {
return None;
}
if is_source_file_read(trimmed) {
return None;
}
let tool = effective_tool_name(trimmed);
if !is_safe_tool_name(&tool) {
return None;
}
if !has_filter_for(&tool) {
return None;
}
Some(format!(
"{} | tm compress --tool \"{tool}\"",
crate::commands::compress::wrap_command_reporting_exit(trimmed)
))
}
pub(crate) fn rewrite_bash_command_unless_isolated(
command: &str,
hook_cwd: Option<&Path>,
) -> Option<String> {
let cwd = hook_cwd.filter(|c| !c.as_os_str().is_empty())?;
if is_isolation_worktree(cwd) {
return None;
}
rewrite_bash_command_for_compression(command)
}
fn is_isolation_worktree(cwd: &Path) -> bool {
worktree_root(cwd)
.and_then(|root| root.parent().map(|p| p.ends_with(".claude/worktrees")))
.unwrap_or(false)
}
pub(crate) fn hook_cwd(payload: Option<&serde_json::Value>) -> Option<PathBuf> {
payload
.and_then(|v| v.get("cwd"))
.and_then(|v| v.as_str())
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
}
pub(crate) fn effective_tool_name(command: &str) -> String {
let words = shlex_words(command);
let mut tokens = words.iter().map(String::as_str);
let Some(mut first) = tokens.next() else {
return String::new();
};
loop {
if is_env_assignment(first) {
let Some(next) = tokens.next() else {
return String::new();
};
first = next;
continue;
}
if first == "sudo" || first == "env" {
match tokens.next() {
Some(next) if !next.starts_with('-') => {
first = next;
continue;
}
_ => return String::new(),
}
}
break;
}
let program = first.rsplit('/').next().unwrap_or(first);
match tokens.next() {
Some(subcommand) => format!("{program} {subcommand}"),
None => program.to_string(),
}
}
fn is_safe_tool_name(tool_name: &str) -> bool {
!tool_name.is_empty()
&& tool_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '-' | '_' | '.' | '/' | ':'))
}
fn is_orchestrator_command(command: &str) -> bool {
match first_command_token(command) {
Some(program) => ORCHESTRATOR_EXCLUSIONS.contains(&program.as_str()),
None => true,
}
}
pub(crate) use super::program_word::COMMAND_WRAPPERS;
pub(crate) fn strip_wrapper_prefix<T: AsRef<str>>(tokens: &[T]) -> Option<usize> {
super::program_word::precommand_index(tokens)
}
pub(crate) fn first_command_token(command: &str) -> Option<String> {
program_basename(&shlex_words(command))
}
fn shlex_words(command: &str) -> Vec<String> {
shlex::split(command).unwrap_or_else(|| {
command
.split_whitespace()
.map(str::to_string)
.collect::<Vec<_>>()
})
}
fn program_basename(tokens: &[String]) -> Option<String> {
let start = strip_wrapper_prefix(tokens)?;
let tok = tokens.get(start)?.as_str();
let tok = tok.strip_prefix('\\').unwrap_or(tok);
Some(tok.rsplit('/').next().unwrap_or(tok).to_string())
}
pub(crate) fn is_env_assignment(token: &str) -> bool {
let Some((key, _value)) = token.split_once('=') else {
return false;
};
!key.is_empty()
&& key
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn has_unsafe_pipe_composition(command: &str) -> bool {
command.contains('|')
|| command.contains('&') || command.contains(';')
|| command.contains('>')
|| command.contains('<')
}
fn cannot_be_brace_wrapped(command: &str) -> bool {
let trailing_backslashes = command.chars().rev().take_while(|c| *c == '\\').count();
command.contains('#') || trailing_backslashes % 2 == 1
}
pub(crate) fn build_pretooluse_rewrite_response(rewritten_command: &str) -> serde_json::Value {
serde_json::json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": { "command": rewritten_command }
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn expected_rewrite(command: &str, tool: &str) -> String {
format!(
"{} | tm compress --tool \"{tool}\"",
crate::commands::compress::wrap_command_reporting_exit(command)
)
}
#[test]
fn rewrite_appends_compress_pipe_for_plain_command() {
let out = rewrite_bash_command_for_compression("cargo test");
assert_eq!(
out.as_deref(),
Some(expected_rewrite("cargo test", "cargo test").as_str())
);
let out = out.expect("a covered tool must be wrapped");
assert!(out.starts_with("{ cargo test; printf "), "{out}");
assert!(
out.ends_with("} | tm compress --tool \"cargo test\""),
"{out}"
);
}
#[test]
fn rewrite_appends_compress_pipe_with_subcommand_tool_name() {
let out = rewrite_bash_command_for_compression("git diff HEAD~1");
assert_eq!(
out.as_deref(),
Some(expected_rewrite("git diff HEAD~1", "git diff").as_str())
);
}
const ISOLATION_REFUSED: [&str; 3] = ["git diff", "ls -la", "cargo test"];
#[test]
fn no_rewrite_inside_an_isolation_worktree() {
for cwd in [
"/repo/.claude/worktrees/agent-a",
"/repo/.claude/worktrees/agent-a/crates/trusty-mpm",
"/repo/.base/.worktrees/session-x/.claude/worktrees/agent-b",
] {
for cmd in ISOLATION_REFUSED {
assert!(
rewrite_bash_command_for_compression(cmd).is_some(),
"{cmd} must be a covered tool, or this test proves nothing"
);
assert_eq!(
rewrite_bash_command_unless_isolated(cmd, Some(Path::new(cwd))),
None,
"{cmd} in {cwd}"
);
}
}
}
#[test]
fn rewrite_outside_an_isolation_worktree_is_unchanged() {
for cwd in [
"/repo",
"/repo/crates/trusty-mpm",
"/repo/.base/.worktrees/session-x",
"/repo/.claude/worktrees",
"/repo/worktrees/agent-a",
] {
for cmd in ISOLATION_REFUSED {
assert_eq!(
rewrite_bash_command_unless_isolated(cmd, Some(Path::new(cwd))),
rewrite_bash_command_for_compression(cmd),
"{cmd} in {cwd}"
);
}
}
}
#[test]
fn no_rewrite_when_the_cwd_is_undecidable() {
for cmd in ISOLATION_REFUSED {
assert_eq!(rewrite_bash_command_unless_isolated(cmd, None), None);
assert_eq!(
rewrite_bash_command_unless_isolated(cmd, Some(Path::new(""))),
None
);
}
}
#[test]
fn cannot_be_brace_wrapped_counts_trailing_backslashes() {
assert!(cannot_be_brace_wrapped("ls -la \\"));
assert!(!cannot_be_brace_wrapped("ls -la \\\\"));
assert!(cannot_be_brace_wrapped("ls -la \\\\\\"));
assert!(!cannot_be_brace_wrapped("grep -n hi\\ there f"));
assert!(cannot_be_brace_wrapped("cargo test # explain"));
assert!(cannot_be_brace_wrapped("grep -c '#include' f"));
assert!(!cannot_be_brace_wrapped("cargo test -p trusty-mpm"));
}
#[test]
fn rewrite_skips_a_command_a_brace_group_cannot_wrap() {
for cmd in [
"cargo test # explain",
"grep -c '#include' src/lib.rs",
"cargo test -p x \\",
"git diff HEAD~1 \\",
] {
assert_eq!(
rewrite_bash_command_for_compression(cmd),
None,
"a brace group cannot wrap this safely: {cmd}"
);
}
}
fn shell_is_available(shell: &str) -> bool {
std::process::Command::new(shell)
.arg("-c")
.arg("exit 0")
.output()
.is_ok()
}
fn run_script(shell: &str, script: &str) -> (Option<i32>, String, String) {
let out = std::process::Command::new(shell)
.arg("-c")
.arg(script)
.output()
.unwrap_or_else(|e| panic!("failed to run {shell}: {e}"));
(
out.status.code(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn every_rewrite_this_module_emits_runs_correctly_in_a_real_shell() {
let dir = tempfile::tempdir().expect("temp dir");
let payload = dir.path().join("payload.txt");
std::fs::write(&payload, "alpha\nhi there\nomega\n").expect("write payload");
let path = payload.display().to_string();
let plain = format!("grep -n hi {path}");
let cases = [
(plain.clone(), "2:hi there"),
(format!("{plain} # explain"), "2:hi there"),
(format!("grep -c '#include' {path}"), "0"),
(format!("{plain} \\"), "hi there"),
];
for shell in ["sh", "bash", "zsh"] {
if !shell_is_available(shell) {
eprintln!(
"SKIP every_rewrite_this_module_emits_runs_correctly_in_a_real_shell: no {shell} on this host"
);
continue;
}
for (command, expected) in &cases {
let Some(rewritten) = rewrite_bash_command_for_compression(command) else {
let (_code, stdout, stderr) = run_script(shell, command);
assert!(
stdout.contains(expected),
"{shell}: an un-rewritten `{command}` must still run: \
stdout={stdout:?} stderr={stderr:?}"
);
assert!(
!stdout.contains("__tm_compress_exit=")
&& !stderr.contains("__tm_compress_exit="),
"{shell}: nothing from the template may reach an \
un-rewritten command: stdout={stdout:?} stderr={stderr:?}"
);
continue;
};
let (group, _filter) = rewritten
.split_once("| tm compress")
.expect("the rewrite always ends in the compress filter stage");
let script = format!("{group}| cat");
let (code, stdout, stderr) = run_script(shell, &script);
assert_eq!(
code,
Some(0),
"{shell}: the rewritten `{command}` must run cleanly: \
script={script:?} stdout={stdout:?} stderr={stderr:?}"
);
assert!(
stdout.starts_with(expected),
"{shell}: the command's own output must survive the wrap: \
stdout={stdout:?} stderr={stderr:?}"
);
assert!(
stdout.ends_with("\n__tm_compress_exit=0__\n"),
"{shell}: the sentinel must reach the filter intact: \
stdout={stdout:?} stderr={stderr:?}"
);
}
}
}
#[test]
fn rewrite_skips_orchestrator_commands() {
for cmd in [
"make build",
"sam deploy",
"terraform apply",
"gradlew build",
] {
assert_eq!(
rewrite_bash_command_for_compression(cmd),
None,
"expected no rewrite for orchestrator command: {cmd}"
);
}
}
#[test]
fn rewrite_skips_piped_commands() {
assert_eq!(
rewrite_bash_command_for_compression("cargo test | tee out.log"),
None
);
}
#[test]
fn rewrite_skips_chained_commands() {
assert_eq!(
rewrite_bash_command_for_compression("cargo build && cargo test"),
None
);
assert_eq!(rewrite_bash_command_for_compression("cd /tmp; ls"), None);
}
#[test]
fn rewrite_skips_empty_command() {
assert_eq!(rewrite_bash_command_for_compression(""), None);
assert_eq!(rewrite_bash_command_for_compression(" "), None);
}
#[test]
fn rewrite_skips_command_substitution_in_tool_name() {
assert_eq!(
rewrite_bash_command_for_compression("echo $(touch /tmp/pwned)"),
None
);
assert_eq!(rewrite_bash_command_for_compression("echo `whoami`"), None);
assert_eq!(
rewrite_bash_command_for_compression("echo \"quoted\" arg"),
None
);
}
#[test]
fn rewrite_skips_sudo_with_user_flag() {
assert_eq!(
rewrite_bash_command_for_compression("sudo -u root make build"),
None
);
}
#[test]
fn rewrite_skips_tools_with_no_compression_filter() {
for cmd in [
"git status",
"git add -A",
"git push",
"sed -n 1p f",
"gh pr list",
"gh issue view 6566",
"wc -l Cargo.toml",
"ssh host uptime",
"tm wait --for run",
"echo hello",
] {
assert_eq!(
rewrite_bash_command_for_compression(cmd),
None,
"expected no compress wrap for uncovered tool: {cmd}"
);
}
}
#[test]
fn rewrite_skips_a_flagged_source_read() {
for cmd in [
"cat -n crates/trusty-mpm/src/bin/tm/commands/hook_rewrite.rs",
"cat crates/trusty-mpm/src/bin/tm/commands/hook_rewrite.rs",
"cat -s src/lib.rs",
"/bin/cat -n README.md",
] {
assert_eq!(
rewrite_bash_command_for_compression(cmd),
None,
"a source read must never be wrapped for compression: {cmd}"
);
}
let expected = expected_rewrite("cat -n /tmp/gate.txt", "cat -n");
assert_eq!(
rewrite_bash_command_for_compression("cat -n /tmp/gate.txt").as_deref(),
Some(expected.as_str()),
"a captured gate log is not a source read"
);
}
#[test]
fn rewrite_still_wraps_tools_with_a_compression_filter() {
for (cmd, tool) in [
("cargo test -p x", "cargo test"),
("cargo clippy --all-targets", "cargo clippy"),
("git diff", "git diff"),
("git log --oneline", "git log"),
("grep -n foo src/", "grep -n"),
("ls -la", "ls -la"),
] {
let expected = expected_rewrite(cmd, tool);
assert_eq!(
rewrite_bash_command_for_compression(cmd).as_deref(),
Some(expected.as_str()),
"expected a compress wrap for covered tool: {cmd}"
);
}
}
#[test]
fn is_orchestrator_command_matches_known_exclusions() {
assert!(is_orchestrator_command("make"));
assert!(is_orchestrator_command("sudo make install"));
assert!(is_orchestrator_command("FOO=bar make build"));
assert!(is_orchestrator_command("/usr/bin/make build"));
}
#[test]
fn is_orchestrator_command_ignores_unrelated_commands() {
assert!(!is_orchestrator_command("cargo test"));
assert!(!is_orchestrator_command("git diff"));
}
#[test]
fn is_orchestrator_command_conservatively_true_for_sudo_with_flags() {
assert!(is_orchestrator_command("sudo -u root make build"));
assert!(is_orchestrator_command("env -i make build"));
}
#[test]
fn first_command_token_strips_env_assignment() {
assert_eq!(
first_command_token("FOO=bar make build").as_deref(),
Some("make")
);
}
#[test]
fn first_command_token_strips_sudo() {
assert_eq!(
first_command_token("sudo make install").as_deref(),
Some("make")
);
}
#[test]
fn first_command_token_strips_path() {
assert_eq!(
first_command_token("/usr/bin/make build").as_deref(),
Some("make")
);
}
#[test]
fn first_command_token_plain_command() {
assert_eq!(first_command_token("cargo test").as_deref(), Some("cargo"));
}
#[test]
fn first_command_token_strips_env_command_prefix() {
assert_eq!(
first_command_token("env FOO=bar make build").as_deref(),
Some("make")
);
}
#[test]
fn first_command_token_none_for_sudo_followed_by_flag() {
assert_eq!(
first_command_token("sudo -u root make build").as_deref(),
None
);
assert_eq!(first_command_token("env -i make build").as_deref(), None);
}
#[test]
fn first_command_token_strips_leading_backslash() {
assert_eq!(first_command_token("\\rm -rf /root").as_deref(), Some("rm"));
}
#[test]
fn first_command_token_strips_command_wrapper() {
assert_eq!(
first_command_token("command rm -rf /root").as_deref(),
Some("rm")
);
}
#[test]
fn first_command_token_strips_builtin_wrapper() {
assert_eq!(
first_command_token("builtin rm -rf /root").as_deref(),
Some("rm")
);
}
#[test]
fn first_command_token_strips_backslash_after_sudo() {
assert_eq!(
first_command_token("sudo \\rm -rf /root").as_deref(),
Some("rm")
);
}
#[test]
fn first_command_token_strips_nice_time_nohup_exec() {
for command in [
"nice rm -rf /root",
"time rm -rf /root",
"nohup rm -rf /root",
"exec rm -rf /root",
] {
assert_eq!(
first_command_token(command).as_deref(),
Some("rm"),
"expected \"rm\" for: {command}"
);
}
}
#[test]
fn first_command_token_resolves_a_quoted_path_with_spaces() {
for command in [
r#""/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless"#,
r#"'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --headless"#,
] {
assert_eq!(
first_command_token(command).as_deref(),
Some("Google Chrome"),
"expected the path's own basename for: {command}"
);
}
assert_eq!(
first_command_token("/Applications/GoogleChrome.app/Contents/MacOS/GoogleChrome -x")
.as_deref(),
Some("GoogleChrome")
);
}
#[test]
fn first_command_token_resolves_a_quoted_forbidden_verb() {
assert_eq!(
first_command_token(r#""/Applications/My Tools/sed" -i s/a/b/ f"#).as_deref(),
Some("sed")
);
assert_eq!(
first_command_token(r#"'/Applications/My Tools/patch' -p1 x"#).as_deref(),
Some("patch")
);
assert_eq!(
first_command_token(r#""/opt/make tools/echo" hi"#).as_deref(),
Some("echo")
);
assert_eq!(
first_command_token(r#""sudo" rm -rf /root"#).as_deref(),
Some("rm")
);
}
#[test]
fn first_command_token_resolves_split_quoting() {
for command in [
r#"s"e"d -i s/a/b/ f"#,
r#"'se'"d" -i s/a/b/ f"#,
r#""se"'d' -i s/a/b/ f"#,
r"/opt/my\ tools/sed -i x f",
] {
assert_eq!(
first_command_token(command).as_deref(),
Some("sed"),
"split quoting must not hide the verb: {command}"
);
}
assert_eq!(
first_command_token(r#"c"u"rl http://x"#).as_deref(),
Some("curl")
);
}
#[test]
fn first_command_token_keeps_the_fallback_answer_on_unbalanced_quoting() {
assert!(shlex::split(r#"sed -i "x"#).is_none());
assert_eq!(
first_command_token(r#"sed -i "x"#).as_deref(),
Some("sed"),
"an unbalanced quote after the verb must not lose the deny"
);
assert_eq!(
first_command_token(r#""sed -i x"#).as_deref(),
Some("\"sed"),
"the fallback answers exactly as split_whitespace did"
);
}
#[test]
fn shlex_words_lexes_split_quoting() {
assert_eq!(shlex_words(r#"s"e"d -i f"#), vec!["sed", "-i", "f"]);
assert_eq!(
shlex_words(r#""/opt/a b/sed" -i f"#),
vec!["/opt/a b/sed", "-i", "f"]
);
assert_eq!(
shlex_words("git diff -- docs/"),
vec!["git", "diff", "--", "docs/"]
);
}
#[test]
fn shlex_words_falls_back_on_unbalanced_quoting() {
assert_eq!(shlex_words(r#"sed -i "x"#), vec!["sed", "-i", "\"x"]);
}
#[test]
fn effective_tool_name_lexes_quoted_and_split_spellings() {
assert_eq!(
effective_tool_name(r#""/opt/My Tools/npm" test"#),
"npm test"
);
assert_eq!(effective_tool_name(r#"n"p"m test"#), "npm test");
assert_eq!(effective_tool_name(r"/opt/my\ tools/npm test"), "npm test");
}
#[test]
fn strip_wrapper_prefix_skips_env_assignment() {
let tokens = ["FOO=bar", "make", "build"];
assert_eq!(strip_wrapper_prefix(&tokens), Some(1));
}
#[test]
fn strip_wrapper_prefix_skips_every_known_wrapper() {
for wrapper in COMMAND_WRAPPERS {
let operand = crate::commands::program_word::sample_operand(wrapper);
let (tokens, program) = if operand.is_empty() {
(vec![*wrapper, "rm", "-rf", "/root"], 1)
} else {
(vec![*wrapper, operand, "rm", "-rf", "/root"], 2)
};
assert_eq!(
strip_wrapper_prefix(&tokens),
Some(program),
"expected wrapper {wrapper} to be skipped"
);
}
}
#[test]
fn strip_wrapper_prefix_none_for_wrapper_followed_by_flag() {
let tokens = ["sudo", "-u", "root", "make"];
assert_eq!(strip_wrapper_prefix(&tokens), None);
}
#[test]
fn strip_wrapper_prefix_skips_backslash_before_a_wrapper() {
let tokens = ["\\nice", "rm", "-rf", "/root"];
assert_eq!(strip_wrapper_prefix(&tokens), Some(1));
}
#[test]
fn effective_tool_name_joins_program_and_subcommand() {
assert_eq!(effective_tool_name("cargo test --workspace"), "cargo test");
assert_eq!(effective_tool_name("git diff HEAD~1"), "git diff");
}
#[test]
fn effective_tool_name_single_token_command() {
assert_eq!(effective_tool_name("ls"), "ls");
assert_eq!(effective_tool_name("ls -la"), "ls -la");
}
#[test]
fn effective_tool_name_strips_env_and_sudo_noise() {
assert_eq!(effective_tool_name("FOO=bar cargo test"), "cargo test");
assert_eq!(effective_tool_name("sudo cargo test"), "cargo test");
assert_eq!(effective_tool_name("/usr/bin/cargo test"), "cargo test");
}
#[test]
fn effective_tool_name_strips_env_command_prefix() {
assert_eq!(effective_tool_name("env FOO=bar cargo test"), "cargo test");
}
#[test]
fn effective_tool_name_returns_empty_for_sudo_followed_by_flag() {
assert_eq!(effective_tool_name("sudo -u root make build"), "");
assert_eq!(effective_tool_name("env -i cargo test"), "");
}
#[test]
fn is_safe_tool_name_allows_typical_tool_names() {
assert!(is_safe_tool_name("cargo test"));
assert!(is_safe_tool_name("git diff"));
assert!(is_safe_tool_name("./scripts/foo.sh"));
assert!(is_safe_tool_name("ls-la_v2"));
}
#[test]
fn is_safe_tool_name_rejects_shell_metacharacters() {
assert!(!is_safe_tool_name("echo $(touch /tmp/pwned)"));
assert!(!is_safe_tool_name("echo `whoami`"));
assert!(!is_safe_tool_name("echo \"quoted\""));
assert!(!is_safe_tool_name("a;b"));
assert!(!is_safe_tool_name(""));
}
#[test]
fn has_unsafe_pipe_composition_detects_pipe() {
assert!(has_unsafe_pipe_composition("cargo test | tee out.log"));
}
#[test]
fn has_unsafe_pipe_composition_detects_and_chain() {
assert!(has_unsafe_pipe_composition("cargo build && cargo test"));
}
#[test]
fn has_unsafe_pipe_composition_detects_semicolon() {
assert!(has_unsafe_pipe_composition("cd /tmp; ls"));
}
#[test]
fn has_unsafe_pipe_composition_detects_output_redirection() {
assert!(has_unsafe_pipe_composition("cargo test > out.log"));
assert!(has_unsafe_pipe_composition("cargo test >> out.log"));
}
#[test]
fn has_unsafe_pipe_composition_detects_input_redirection() {
assert!(has_unsafe_pipe_composition("cat < input.txt"));
}
#[test]
fn has_unsafe_pipe_composition_detects_background_operator() {
assert!(has_unsafe_pipe_composition("cargo test &"));
}
#[test]
fn has_unsafe_pipe_composition_false_for_plain_command() {
assert!(!has_unsafe_pipe_composition("cargo test"));
}
#[test]
fn build_pretooluse_rewrite_response_has_expected_shape() {
let response = build_pretooluse_rewrite_response("cargo test | tm compress --tool bash");
assert_eq!(
response["hookSpecificOutput"]["hookEventName"],
"PreToolUse"
);
assert_eq!(
response["hookSpecificOutput"]["updatedInput"]["command"],
"cargo test | tm compress --tool bash"
);
}
}