-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcheck-dangerous-commands.py
More file actions
130 lines (106 loc) · 4.02 KB
/
check-dangerous-commands.py
File metadata and controls
130 lines (106 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env python3
"""
Claude Code PreToolUse hook - cross-platform dangerous command checker
Works on macOS, Linux, and Windows (native or WSL)
"""
import json
import sys
import re
import os
import shlex
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from secrets_patterns import contains_secrets_reference, is_secrets_path
SHELL_OPERATORS = {"|", "||", "&", "&&", ";", ">", ">>", "<", "<<"}
def tokenize_command(command: str) -> list[str]:
"""Split a command conservatively for Unix and Windows-like shells."""
for posix in (True, False):
try:
tokens = shlex.split(command, posix=posix)
except ValueError:
continue
if tokens:
return tokens
return re.findall(r'"[^"]*"|\'[^\']*\'|\S+', command)
def looks_like_path_fragment(text: str) -> bool:
"""Heuristic: treat plain path-like args differently from code expressions."""
return not any(ch in text for ch in "()=:,")
def command_touches_secret(command: str) -> bool:
"""Block commands that reference secret-looking paths anywhere in their args."""
for token in tokenize_command(command):
cleaned = token.strip("\"'`()[]{} ,")
cleaned = cleaned.rstrip(";|&<>")
if not cleaned or cleaned.startswith("-") or cleaned in SHELL_OPERATORS:
continue
if (
(looks_like_path_fragment(cleaned) and is_secrets_path(cleaned))
or contains_secrets_reference(token)
or contains_secrets_reference(cleaned)
):
return True
return False
def check_command(command: str) -> tuple[bool, str]:
"""Returns (blocked, reason)"""
checks = [
# Recursive force deletes on root/home (handles both -rf and -fr)
(
r'rm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+\s+(/\s*$|/\s*\*|~/?\s*$|~/?\s*\*)',
"Blocked: recursive force delete on root or home directory"
),
# Recursive force deletes on system directories
(
r'rm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+\s+/(home|var|opt|usr|etc|boot|lib|sbin|root)\b',
"Blocked: recursive force delete on system directory"
),
# Windows recursive force delete
(
r'(Remove-Item|ri)\s+.*-Recurse.*-Force\s+(C:\\\\?|~)',
"Blocked: recursive force delete on root or home (Windows)"
),
# Pipe-to-shell (supply chain risk)
(
r'(curl|wget|iwr|Invoke-WebRequest).+\|\s*(bash|sh|zsh|python3?|node|iex)',
"Blocked: pipe-to-shell pattern detected (supply chain risk)"
),
# chmod 777
(
r'chmod\s+(-R\s+)?777',
"Blocked: chmod 777 is a security risk"
),
# Writing to unix system dirs
(
r'(>>?|tee)\s+/(etc|usr|bin|sbin|lib|boot)/',
"Blocked: write to system directory"
),
# Writing to Windows system dirs
(
r'(>>?|Out-File|Set-Content|Add-Content)\s+["\']?C:\\(Windows|System32|Program Files)',
"Blocked: write to Windows system directory"
),
# Dropping/truncating databases (extra caution)
(
r'DROP\s+(DATABASE|TABLE|SCHEMA)\s+\w+',
"Blocked: destructive SQL statement — confirm manually if intentional"
),
]
for pattern, reason in checks:
if re.search(pattern, command, re.IGNORECASE):
return True, reason
if command_touches_secret(command):
return True, "Blocked: command references potential secrets file"
return False, ""
def main():
try:
data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
sys.exit(0) # Can't parse input — allow and move on
command = data.get("tool_input", {}).get("command", "")
if not command:
sys.exit(0)
blocked, reason = check_command(command)
if blocked:
response = {"decision": "block", "reason": reason}
print(json.dumps(response))
sys.exit(2)
sys.exit(0)
if __name__ == "__main__":
main()