Skip to main content

ai_agent/tools/powershell/
destructive_command_warning.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/tools/PowerShellTool/destructiveCommandWarning.ts
2//! Detects potentially destructive PowerShell commands and returns a warning
3
4use once_cell::sync::Lazy;
5use regex::Regex;
6
7/// Destructive pattern with warning message
8struct DestructivePattern {
9    pattern: Regex,
10    warning: &'static str,
11}
12
13/// All destructive patterns
14fn get_destructive_patterns() -> Vec<DestructivePattern> {
15    vec![
16        // Remove-Item with -Recurse and/or -Force
17        DestructivePattern {
18            pattern: Regex::new(r"(?:^|[|;&\n({])\s*(Remove-Item|rm|del|rd|rmdir|ri)\b[^|;&\n}]*-Recurse\b[^|;&\n}]*-Force\b").unwrap(),
19            warning: "Note: may recursively force-remove files",
20        },
21        DestructivePattern {
22            pattern: Regex::new(r"(?:^|[|;&\n({])\s*(Remove-Item|rm|del|rd|rmdir|ri)\b[^|;&\n}]*-Force\b[^|;&\n}]*-Recurse\b").unwrap(),
23            warning: "Note: may recursively force-remove files",
24        },
25        DestructivePattern {
26            pattern: Regex::new(r"(?:^|[|;&\n({])\s*(Remove-Item|rm|del|rd|rmdir|ri)\b[^|;&\n}]*-Recurse\b").unwrap(),
27            warning: "Note: may recursively remove files",
28        },
29        DestructivePattern {
30            pattern: Regex::new(r"(?:^|[|;&\n({])\s*(Remove-Item|rm|del|rd|rmdir|ri)\b[^|;&\n}]*-Force\b").unwrap(),
31            warning: "Note: may force-remove files",
32        },
33        // Clear-Content on broad paths
34        DestructivePattern {
35            pattern: Regex::new(r"\bClear-Content\b[^|;&\n]*\*").unwrap(),
36            warning: "Note: may clear content of multiple files",
37        },
38        // Format-Volume and Clear-Disk
39        DestructivePattern {
40            pattern: Regex::new(r"\bFormat-Volume\b").unwrap(),
41            warning: "Note: may format a disk volume",
42        },
43        DestructivePattern {
44            pattern: Regex::new(r"\bClear-Disk\b").unwrap(),
45            warning: "Note: may clear a disk",
46        },
47        // Git destructive operations
48        DestructivePattern {
49            pattern: Regex::new(r"\bgit\s+reset\s+--hard\b").unwrap(),
50            warning: "Note: may discard uncommitted changes",
51        },
52        DestructivePattern {
53            pattern: Regex::new(r"\bgit\s+push\b[^|;&\n]*\s+(--force|--force-with-lease|-f)\b").unwrap(),
54            warning: "Note: may overwrite remote history",
55        },
56        // Note: git clean -f pattern handled manually due to negative lookahead
57        DestructivePattern {
58            pattern: Regex::new(r"\bgit\s+stash\s+(drop|clear)\b").unwrap(),
59            warning: "Note: may permanently remove stashed changes",
60        },
61        // Database operations
62        DestructivePattern {
63            pattern: Regex::new(r"\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b").unwrap(),
64            warning: "Note: may drop or truncate database objects",
65        },
66        // System operations
67        DestructivePattern {
68            pattern: Regex::new(r"\bStop-Computer\b").unwrap(),
69            warning: "Note: will shut down the computer",
70        },
71        DestructivePattern {
72            pattern: Regex::new(r"\bRestart-Computer\b").unwrap(),
73            warning: "Note: will restart the computer",
74        },
75        DestructivePattern {
76            pattern: Regex::new(r"\bClear-RecycleBin\b").unwrap(),
77            warning: "Note: permanently deletes recycled files",
78        },
79    ]
80}
81
82static DESTRUCTIVE_PATTERNS: Lazy<Vec<DestructivePattern>> = Lazy::new(get_destructive_patterns);
83
84/// Checks if a PowerShell command matches known destructive patterns.
85/// Returns a human-readable warning string, or None if no destructive pattern is detected.
86pub fn get_destructive_command_warning(command: &str) -> Option<&'static str> {
87    for pattern in DESTRUCTIVE_PATTERNS.iter() {
88        if pattern.pattern.is_match(command) {
89            return Some(pattern.warning);
90        }
91    }
92
93    // Manual check for git clean -f (without -n or --dry-run)
94    let lower = command.to_lowercase();
95    if lower.contains("git") && lower.contains("clean") {
96        // Check for -f without -n or --dry-run
97        let has_f = lower.contains(" -f ")
98            || lower.contains(" -f\n")
99            || lower.contains(" -f\t")
100            || lower.contains(" --force")
101            || lower.contains(" -fd");
102        let has_dry_run = lower.contains(" -n ") || lower.contains(" --dry-run");
103        if has_f && !has_dry_run {
104            return Some("Note: may permanently delete untracked files");
105        }
106    }
107
108    None
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_remove_item_recursive_force() {
117        let warning = get_destructive_command_warning("Remove-Item -Path ./foo -Recurse -Force");
118        assert!(warning.is_some());
119    }
120
121    #[test]
122    fn test_git_reset_hard() {
123        let warning = get_destructive_command_warning("git reset --hard");
124        assert!(warning.is_some());
125    }
126
127    #[test]
128    fn test_safe_command() {
129        let warning = get_destructive_command_warning("Get-ChildItem");
130        assert!(warning.is_none());
131    }
132}