use super::*;
#[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 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 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_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"
);
}
}