3030# Paths always allowed regardless of working directory
3131_home = os .environ .get ("HOME" , "/home/vscode" )
3232ALLOWED_PREFIXES = [
33- f"{ _home } /.claude/" , # Claude config, plans, rules
34- "/tmp/" , # System scratch
33+ f"{ _home } /.claude/" , # Claude config, plans, rules
34+ "/tmp/" , # System scratch
3535]
3636
3737WRITE_TOOLS = {"Write" , "Edit" , "NotebookEdit" }
5454# ---------------------------------------------------------------------------
5555WRITE_PATTERNS = [
5656 # --- Ported from guard-protected-bash.py ---
57- r"(?:>|>> )\s*([^\s;&|]+)" , # > file, > > file
58- r"\btee\s+(?:-a\s+)?([^\s;&|]+)" , # tee file
59- r"\b(?:cp|mv)\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # cp/mv src dest
57+ r"(?:>>|> )\s*([^\s;&|]+)" , # >> file, > file
58+ r"\btee\s+(?:-a\s+)?([^\s;&|]+)" , # tee file
59+ r"\b(?:cp|mv)\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # cp/mv src dest
6060 r'\bsed\s+-i[^\s]*\s+(?:\'[^\']*\'\s+|"[^"]*"\s+|[^\s]+\s+)*([^\s;&|]+)' , # sed -i
61- r"\bcat\s+(?:<<[^\s]*\s+)?>\s*([^\s;&|]+)" , # cat > file
61+ r"\bcat\s+(?:<<[^\s]*\s+)?>\s*([^\s;&|]+)" , # cat > file
6262 # --- New patterns ---
63- r"\btouch\s+(?:-[^\s]+\s+)*([^\s;&|]+)" , # touch file
64- r"\bmkdir\s+(?:-[^\s]+\s+)*([^\s;&|]+)" , # mkdir [-p] dir
65- r"\brm\s+(?:-[^\s]+\s+)*([^\s;&|]+)" , # rm [-rf] path
66- r"\bln\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # ln [-s] src dest
67- r"\binstall\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # install src dest
68- r"\brsync\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # rsync src dest
69- r"\bchmod\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # chmod mode path
70- r"\bchown\s+(?:-[^\s]+\s+)*[^\s:]+(?::[^\s]+)?\s+([^\s;&|]+)" , # chown owner[:group] path
71- r"\bdd\b[^;|&]*\bof=([^\s;&|]+)" , # dd of=path
72- r"\bwget\s+(?:-[^\s]+\s+)*-O\s+([^\s;&|]+)" , # wget -O path
73- r"\bcurl\s+(?:-[^\s]+\s+)*-o\s+([^\s;&|]+)" , # curl -o path
74- r"\btar\s+(?:-[^\s]+\s+)*-C\s+([^\s;&|]+)" , # tar -C dir
75- r"\bunzip\s+(?:-[^\s]+\s+)*-d\s+([^\s;&|]+)" , # unzip -d dir
63+ r"\btouch\s+(?:-[^\s]+\s+)*([^\s;&|]+)" , # touch file
64+ r"\bmkdir\s+(?:-[^\s]+\s+)*([^\s;&|]+)" , # mkdir [-p] dir
65+ r"\brm\s+(?:-[^\s]+\s+)*([^\s;&|]+)" , # rm [-rf] path
66+ r"\bln\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # ln [-s] src dest
67+ r"\binstall\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # install src dest
68+ r"\brsync\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # rsync src dest
69+ r"\bchmod\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)" , # chmod mode path
70+ r"\bchown\s+(?:-[^\s]+\s+)*[^\s:]+(?::[^\s]+)?\s+([^\s;&|]+)" , # chown owner[:group] path
71+ r"\bdd\b[^;|&]*\bof=([^\s;&|]+)" , # dd of=path
72+ r"\bwget\s+(?:-[^\s]+\s+)*-O\s+([^\s;&|]+)" , # wget -O path
73+ r"\bcurl\s+(?:-[^\s]+\s+)*-o\s+([^\s;&|]+)" , # curl -o path
74+ r"\btar\s+(?:-[^\s]+\s+)*-C\s+([^\s;&|]+)" , # tar -C dir
75+ r"\bunzip\s+(?:-[^\s]+\s+)*-d\s+([^\s;&|]+)" , # unzip -d dir
7676 r"\b(?:gcc|g\+\+|cc|c\+\+|clang)\s+(?:-[^\s]+\s+)*-o\s+([^\s;&|]+)" , # gcc -o out
77- r"\bsqlite3\s+([^\s;&|]+)" , # sqlite3 dbpath
77+ r"\bsqlite3\s+([^\s;&|]+)" , # sqlite3 dbpath
7878]
7979
8080# ---------------------------------------------------------------------------
8686# ---------------------------------------------------------------------------
8787# System command exemption (Layer 1 only)
8888# ---------------------------------------------------------------------------
89- SYSTEM_COMMANDS = frozenset ({
90- "git" , "pip" , "pip3" , "npm" , "npx" , "yarn" , "pnpm" ,
91- "apt-get" , "apt" , "cargo" , "go" , "docker" , "make" , "cmake" ,
92- "node" , "python3" , "python" , "ruby" , "gem" , "bundle" ,
93- })
89+ SYSTEM_COMMANDS = frozenset (
90+ {
91+ "git" ,
92+ "pip" ,
93+ "pip3" ,
94+ "npm" ,
95+ "npx" ,
96+ "yarn" ,
97+ "pnpm" ,
98+ "apt-get" ,
99+ "apt" ,
100+ "cargo" ,
101+ "go" ,
102+ "docker" ,
103+ "make" ,
104+ "cmake" ,
105+ "node" ,
106+ "python3" ,
107+ "python" ,
108+ "ruby" ,
109+ "gem" ,
110+ "bundle" ,
111+ }
112+ )
94113
95114SYSTEM_PATH_PREFIXES = (
96- "/usr/" , "/bin/" , "/sbin/" , "/lib/" , "/opt/" ,
97- "/proc/" , "/sys/" , "/dev/" , "/var/" , "/etc/" ,
115+ "/usr/" ,
116+ "/bin/" ,
117+ "/sbin/" ,
118+ "/lib/" ,
119+ "/opt/" ,
120+ "/proc/" ,
121+ "/sys/" ,
122+ "/dev/" ,
123+ "/var/" ,
124+ "/etc/" ,
98125)
99126
100127
101128# ---------------------------------------------------------------------------
102129# Core check functions
103130# ---------------------------------------------------------------------------
104131
132+
105133def is_blacklisted (resolved_path : str ) -> bool :
106134 """Check if resolved_path is under a permanently blocked directory."""
107- return (resolved_path == "/workspaces/.devcontainer"
108- or resolved_path .startswith ("/workspaces/.devcontainer/" ))
135+ return resolved_path == "/workspaces/.devcontainer" or resolved_path .startswith (
136+ "/workspaces/.devcontainer/"
137+ )
109138
110139
111140def is_in_scope (resolved_path : str , cwd : str ) -> bool :
@@ -135,6 +164,7 @@ def get_target_path(tool_name: str, tool_input: dict) -> str | None:
135164# Bash enforcement
136165# ---------------------------------------------------------------------------
137166
167+
138168def extract_write_targets (command : str ) -> list [str ]:
139169 """Extract file paths that the command writes to (Layer 1)."""
140170 targets = []
@@ -157,7 +187,11 @@ def extract_primary_command(command: str) -> str:
157187 while i < len (tokens ):
158188 tok = tokens [i ]
159189 # Skip inline variable assignments: VAR=value
160- if "=" in tok and not tok .startswith ("-" ) and tok .split ("=" , 1 )[0 ].isidentifier ():
190+ if (
191+ "=" in tok
192+ and not tok .startswith ("-" )
193+ and tok .split ("=" , 1 )[0 ].isidentifier ()
194+ ):
161195 i += 1
162196 continue
163197 # Skip sudo and its flags
@@ -243,7 +277,9 @@ def check_bash_scope(command: str, cwd: str) -> None:
243277 # Override: if ANY target is under /workspaces/ outside cwd → NOT exempt
244278 if skip_layer1 :
245279 for _ , resolved in resolved_targets :
246- if resolved .startswith ("/workspaces/" ) and not is_in_scope (resolved , cwd ):
280+ if resolved .startswith ("/workspaces/" ) and not is_in_scope (
281+ resolved , cwd
282+ ):
247283 skip_layer1 = False
248284 break
249285
@@ -273,6 +309,7 @@ def check_bash_scope(command: str, cwd: str) -> None:
273309# Main
274310# ---------------------------------------------------------------------------
275311
312+
276313def main ():
277314 try :
278315 input_data = json .load (sys .stdin )
0 commit comments