Skip to content
Open
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
137c466
fix: require approval for force delete on Windows
hdcodedev Dec 28, 2025
5e249d6
fix: handle trailing punctuation in PowerShell force check
hdcodedev Dec 28, 2025
8b7511d
fix: include ']' in trailing punctuation trim and update comment
hdcodedev Dec 28, 2025
cd4d8a1
test: add cases for force delete inside blocks and brackets
hdcodedev Dec 28, 2025
c6645ed
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 8, 2026
48505fa
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 10, 2026
a9e8b4c
fix: use char array instead of closure for clippy lint
hdcodedev Jan 10, 2026
fbf1515
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 10, 2026
c6d5879
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 11, 2026
d1a4ccf
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 12, 2026
d77e326
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 12, 2026
6cd4a65
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 12, 2026
1748b4a
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
9f006c0
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
835c44d
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
ac5457a
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
e087f49
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
c18f615
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
629fcdb
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 13, 2026
710411d
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 14, 2026
04681ee
fix: use exact match for windows dangerous command flags
hdcodedev Jan 14, 2026
1ad1b44
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 14, 2026
c67c7f6
fix(core): scan all cmd tokens for dangerous commands
hdcodedev Jan 14, 2026
d47d1c9
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 14, 2026
cac1bce
fix(core): prevent bypasses in windows dangerous command detection an…
hdcodedev Jan 14, 2026
415646e
Merge branch 'main' into fix/windows-force-delete-approval
hdcodedev Jan 14, 2026
560b222
fix(core): harden and refine windows dangerous command detection
hdcodedev Jan 14, 2026
0af135a
fix: refine CMD parsing to correctly detect force delete commands
hdcodedev Jan 14, 2026
d80e594
fix(core): enhance CMD command detection by refining tokenization and…
hdcodedev Jan 14, 2026
38ff6c7
fix(core): enhance detection of PowerShell force delete cmdlets by re…
hdcodedev Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 142 additions & 4 deletions codex-rs/core/src/command_safety/windows_dangerous_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ fn is_dangerous_powershell(command: &[String]) -> bool {
}
}

// Check for force delete operations (e.g., Remove-Item -Force)
if has_force_delete_cmdlet(&tokens_lc) {
return true;
}

false
}

Expand Down Expand Up @@ -111,11 +116,24 @@ fn is_dangerous_cmd(command: &[String]) -> bool {
return false;
};
// Classic `cmd /c start https://...` ShellExecute path.
if !first_cmd.eq_ignore_ascii_case("start") {
return false;
if first_cmd.eq_ignore_ascii_case("start") {
let remaining: Vec<String> = iter.cloned().collect();
return args_have_url(&remaining);
}

// Force delete: del /f, erase /f
if first_cmd.eq_ignore_ascii_case("del") || first_cmd.eq_ignore_ascii_case("erase") {
let remaining: Vec<String> = iter.cloned().collect();
return has_force_flag_cmd(&remaining);
}
let remaining: Vec<String> = iter.cloned().collect();
args_have_url(&remaining)

// Recursive directory removal: rd /s /q, rmdir /s /q
if first_cmd.eq_ignore_ascii_case("rd") || first_cmd.eq_ignore_ascii_case("rmdir") {
let remaining: Vec<String> = iter.cloned().collect();
return has_recursive_flag_cmd(&remaining) && has_quiet_flag_cmd(&remaining);
}

false
}

fn is_direct_gui_launch(command: &[String]) -> bool {
Expand Down Expand Up @@ -149,6 +167,35 @@ fn is_direct_gui_launch(command: &[String]) -> bool {
false
}

/// Check for PowerShell force delete cmdlets like `Remove-Item -Force`.
fn has_force_delete_cmdlet(tokens: &[String]) -> bool {
// PowerShell cmdlets/aliases for deletion
const DELETE_CMDLETS: &[&str] = &["remove-item", "ri", "rm", "del", "erase", "rd", "rmdir"];
let has_delete = tokens.iter().any(|t| DELETE_CMDLETS.contains(&t.as_str()));
let has_force = tokens
.iter()
.any(|t| t == "-force" || t.starts_with("-force:"));
has_delete && has_force
}

/// Check for /f or /F flag in CMD del/erase arguments.
fn has_force_flag_cmd(args: &[String]) -> bool {
args.iter()
.any(|a| a.eq_ignore_ascii_case("/f") || a.to_ascii_lowercase().contains("/f"))
}

/// Check for /s or /S flag in CMD rd/rmdir arguments.
fn has_recursive_flag_cmd(args: &[String]) -> bool {
args.iter()
.any(|a| a.eq_ignore_ascii_case("/s") || a.to_ascii_lowercase().contains("/s"))
}

/// Check for /q or /Q flag in CMD rd/rmdir arguments.
fn has_quiet_flag_cmd(args: &[String]) -> bool {
args.iter()
.any(|a| a.eq_ignore_ascii_case("/q") || a.to_ascii_lowercase().contains("/q"))
}

fn args_have_url(args: &[String]) -> bool {
args.iter().any(|arg| looks_like_url(arg))
}
Expand Down Expand Up @@ -313,4 +360,95 @@ mod tests {
"."
])));
}

// Force delete tests for PowerShell

#[test]
fn powershell_remove_item_force_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"powershell",
"-Command",
"Remove-Item test -Force"
])));
}

#[test]
fn powershell_remove_item_recurse_force_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"powershell",
"-Command",
"Remove-Item test -Recurse -Force"
])));
}

#[test]
fn powershell_ri_alias_force_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"pwsh",
"-Command",
"ri test -Force"
])));
}

#[test]
fn powershell_remove_item_without_force_is_not_flagged() {
assert!(!is_dangerous_command_windows(&vec_str(&[
"powershell",
"-Command",
"Remove-Item test"
])));
}

// Force delete tests for CMD
#[test]
fn cmd_del_force_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"cmd", "/c", "del", "/f", "test.txt"
])));
}

#[test]
fn cmd_erase_force_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"cmd", "/c", "erase", "/f", "test.txt"
])));
}

#[test]
fn cmd_del_without_force_is_not_flagged() {
assert!(!is_dangerous_command_windows(&vec_str(&[
"cmd", "/c", "del", "test.txt"
])));
}

#[test]
fn cmd_rd_recursive_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"cmd", "/c", "rd", "/s", "/q", "test"
])));
}

#[test]
fn cmd_rd_without_quiet_is_not_flagged() {
assert!(!is_dangerous_command_windows(&vec_str(&[
"cmd", "/c", "rd", "/s", "test"
])));
}

#[test]
fn cmd_rmdir_recursive_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"cmd", "/c", "rmdir", "/s", "/q", "test"
])));
}

// Test exact scenario from issue #8567
#[test]
fn powershell_remove_item_path_recurse_force_is_dangerous() {
assert!(is_dangerous_command_windows(&vec_str(&[
"powershell",
"-Command",
"Remove-Item -Path 'test' -Recurse -Force"
])));
}
}