fix: harden destructive command matching

This commit is contained in:
Haoran
2026-08-26 15:07:59 +08:00
parent 44e33d0ec3
commit 1293797585
55 changed files with 3328 additions and 690 deletions

View File

@@ -54,17 +54,39 @@ def check_deny_list(command: str) -> str | None:
return None
```
**ゲート 2**:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。
**ゲート 2**:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。shell ルールは quoted separator を構文として扱わずに command を分割し、直接 command、`if`/`for` の本体、`cmd /c``sh -c` の payload など、実際に command が実行される位置を確認する。
ここでの matcher は一般的な command 形式を説明するためのものであり、完全な shell parser や security sandbox ではない。
```python
import re
import shlex
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(command, posix=False,
punctuation_chars=SHELL_SEPARATORS)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
try:
tokens = shell_tokens(command)
except ValueError:
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment)
PERMISSION_RULES = [
{
@@ -152,7 +174,7 @@ python s03_permission/code.py
2. `Delete the file test.txt`bash + rm でゲート 2 が発動)
3. `What files are in the current directory?`(読み取り専用、すべて通過)
4. `Try to write a file to /etc/something`(作業ディレクトリ外への書き込みでゲート 2 が発動)
5. Windows では `del test.txt``DEL test.txt` がゲート 2 を発動し、`model``delimiter``echo del test.txt` は発動しない。
5. Windows では `del test.txt``DEL test.txt``if exist test.txt del test.txt` がゲート 2 を発動し、`model``delimiter``echo del test.txt``echo "safe; del test.txt"` は発動しない。
観察のポイント:どの操作がそのまま通過するか? どれに確認が必要か? どれが即座に拒否されるか?

View File

@@ -54,17 +54,39 @@ def check_deny_list(command: str) -> str | None:
return None
```
**Gate 2**: Rule matching — describes "when to ask the user." Each rule specifies a tool and a check condition.
**Gate 2**: Rule matching — describes "when to ask the user." Each rule specifies a tool and a check condition. The shell rule tokenizes commands without treating quoted separators as syntax, then checks executable positions such as direct commands, `if`/`for` bodies, and `cmd /c` or `sh -c` payloads.
This is a teaching-level matcher for common command forms, not a complete shell parser or security sandbox.
```python
import re
import shlex
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(command, posix=False,
punctuation_chars=SHELL_SEPARATORS)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
try:
tokens = shell_tokens(command)
except ValueError:
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment)
PERMISSION_RULES = [
{
@@ -152,7 +174,7 @@ Try these prompts:
2. `Delete the file test.txt` (bash + rm triggers Gate 2)
3. `What files are in the current directory?` (read-only, all pass)
4. `Try to write a file to /etc/something` (writing outside workspace triggers Gate 2)
5. On Windows, `del test.txt` and `DEL test.txt` trigger Gate 2, while `model`, `delimiter`, and `echo del test.txt` do not.
5. On Windows, `del test.txt`, `DEL test.txt`, and `if exist test.txt del test.txt` trigger Gate 2, while `model`, `delimiter`, `echo del test.txt`, and `echo "safe; del test.txt"` do not.
What to watch for: Which operations pass through? Which need your confirmation? Which are denied outright?

View File

@@ -54,17 +54,39 @@ def check_deny_list(command: str) -> str | None:
return None
```
**闸门 2**负责规则匹配,用来描述"什么时候需要问用户"。每条规则指定工具和检查条件。
**闸门 2**负责规则匹配,用来描述"什么时候需要问用户"。每条规则指定工具和检查条件。shell 规则会先拆分命令,但不把引号内的分隔符当成语法,再检查直接命令、`if`/`for` 主体以及 `cmd /c``sh -c` 等真正执行命令的位置。
这里的 matcher 只用于讲解常见命令形式,并不是完整的 shell parser 或安全沙箱。
```python
import re
import shlex
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(command, posix=False,
punctuation_chars=SHELL_SEPARATORS)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
try:
tokens = shell_tokens(command)
except ValueError:
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment)
PERMISSION_RULES = [
{
@@ -152,7 +174,7 @@ python s03_permission/code.py
2. `Delete the file test.txt`bash + rm 会触发闸门 2
3. `What files are in the current directory?`(只读,全部通过)
4. `Try to write a file to /etc/something`(写工作区外,触发闸门 2
5. 在 Windows 上,`del test.txt``DEL test.txt` 会触发闸门 2`model``delimiter``echo del test.txt` 不会。
5. 在 Windows 上,`del test.txt``DEL test.txt``if exist test.txt del test.txt` 会触发闸门 2`model``delimiter``echo del test.txt``echo "safe; del test.txt"` 不会。
观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝?

View File

@@ -33,6 +33,7 @@ Builds on s02 (multi-tool). Usage:
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -153,13 +154,190 @@ def check_deny_list(command: str) -> str | None:
# Gate 2: Rule matching - context-dependent checks
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
PERMISSION_RULES = [

View File

@@ -102,18 +102,7 @@ agent_loop(history)
**PreToolUse / PostToolUse**、ツール実行の前後のフック。s03 の権限チェックロジックは PreToolUse フックに包まれ、さらにログフックと大出力リマインダーが追加される:
```python
import re
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
# PreToolUse: 権限チェックs03 のロジック、ループからフックに移動)
# PreToolUse: 権限チェックs03 から引き継いだ matcher を含む)
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
@@ -144,8 +133,6 @@ register_hook("PreToolUse", log_hook)
register_hook("PostToolUse", large_output_hook)
```
継承された shell rule は大文字小文字を区別せず、command の先頭または shell separator の直後にある完全な `rm`/`del` command word だけを検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
**Stop** はループが終了する直前に発火する。以下の hook は終了時の統計を出力する:
```python

View File

@@ -102,18 +102,7 @@ agent_loop(history)
**PreToolUse / PostToolUse**, hooks before and after tool execution. s03's permission check logic is now wrapped as a PreToolUse hook, plus a logging hook and a large-output reminder:
```python
import re
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
# PreToolUse: permission check (s03 logic, moved from loop to hook)
# PreToolUse: permission check (including the matcher inherited from s03)
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
@@ -144,8 +133,6 @@ register_hook("PreToolUse", log_hook)
register_hook("PostToolUse", large_output_hook)
```
The inherited shell rule is case-insensitive and matches a complete `rm` or `del` command word only at the start of a command or after a shell separator. It does not match `model`, `delimiter`, or `echo del test.txt`.
**Stop** triggers when the loop is about to exit. The following hook prints a cleanup summary:
```python

View File

@@ -102,18 +102,7 @@ agent_loop(history)
**PreToolUse / PostToolUse**,工具执行前后的 hook。s03 的权限检查逻辑现在包装成 PreToolUse hook再加一个日志 hook 和一个大输出提醒:
```python
import re
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
# PreToolUse: 权限检查s03 的逻辑,从循环移到 hook
# PreToolUse: 权限检查(包含从 s03 沿用的 matcher
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
@@ -144,8 +133,6 @@ register_hook("PreToolUse", log_hook)
register_hook("PostToolUse", large_output_hook)
```
沿用的 shell 规则不区分大小写,只在命令开头或 shell 分隔符之后识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被识别为危险命令。
**Stop** 在循环即将退出时触发。以下 hook 打印收尾统计:
```python

View File

@@ -22,6 +22,7 @@ Hooks run callbacks at fixed points in the agent loop:
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -140,14 +141,191 @@ def trigger_hooks(event: str, *args):
# s03 permission check logic, now wrapped as a hook
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -122,10 +122,6 @@ Agent がタスクを受け取った後の典型的な流れ:まず `todo_writ
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみよう
```sh

View File

@@ -122,10 +122,6 @@ Typical flow when the Agent receives a task: first call `todo_write` to list all
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -122,10 +122,6 @@ Agent 收到任务后的典型流程:先调 `todo_write` 列出所有步骤(
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -26,6 +26,7 @@ import ast
import json
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -219,14 +220,191 @@ def trigger_hooks(event: str, *args):
return None
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -88,10 +88,6 @@ TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみよう
```sh

View File

@@ -88,10 +88,6 @@ The parent dispatches `task` through the same handler map as its other tools. Th
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -88,10 +88,6 @@ TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -20,6 +20,7 @@ The subagent has no task tool, so it cannot delegate again.
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -155,14 +156,191 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -116,10 +116,6 @@ def load(self, name: str) -> str:
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみよう
```sh

View File

@@ -116,10 +116,6 @@ def load(self, name: str) -> str:
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -116,10 +116,6 @@ def load(self, name: str) -> str:
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -21,6 +21,7 @@ The model loads the full SKILL.md only when it calls load_skill.
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -242,14 +243,191 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -296,10 +296,6 @@ if compact_requested:
> **s09 との境界:** s08 は現在のセッションにある有限のコンテキストを管理し、再取得できる詳細を圧縮できます。s09 は、圧縮後や次のセッションにも残す情報を保存します。
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```bash

View File

@@ -296,10 +296,6 @@ This leaves no orphaned tool result. It also preserves the record of a file writ
> **Boundary with s09:** s08 manages the limited context of the current session and may discard recoverable details. s09 stores information that must survive compaction and future sessions.
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```bash

View File

@@ -296,10 +296,6 @@ if compact_requested:
> **与 s09 的边界:** s08 管理当前会话的有限上下文压缩时允许舍弃可恢复的细节s09 保存需要跨压缩、跨会话继续存在的信息。
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```bash

View File

@@ -40,6 +40,7 @@ import glob
import json
import os
import re
import shlex
import subprocess
import uuid
from pathlib import Path
@@ -178,14 +179,191 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -170,10 +170,6 @@ except Exception:
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```sh

View File

@@ -170,10 +170,6 @@ The course uses a simple count threshold. A real application must also choose a
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -170,10 +170,6 @@ except Exception:
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -12,6 +12,7 @@ import glob
import json
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -634,14 +635,191 @@ def trigger_hooks(event: str, *args):
return None
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -198,10 +198,6 @@ complete_task(tests.id) # ✓ Completed
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```sh

View File

@@ -198,10 +198,6 @@ Each `create_task` writes a JSON file; `update_task`, `claim_task`, and `complet
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -198,10 +198,6 @@ complete_task(tests.id) # ✓ Completed
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -25,6 +25,7 @@ import glob
import json
import os
import re
import shlex
import secrets
import subprocess
from dataclasses import asdict, dataclass
@@ -444,14 +445,191 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -151,10 +151,6 @@ npm install がバックグラウンドで実行されている間、Agent Loop
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```sh

View File

@@ -151,10 +151,6 @@ While npm install ran in the background, the Agent Loop continued with read_file
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -151,10 +151,6 @@ npm install 在后台运行时Agent Loop 继续执行了 read_file。
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -15,6 +15,7 @@ import atexit
import glob
import os
import re
import shlex
import signal
import subprocess
import threading
@@ -226,14 +227,191 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def permission_hook(block):

View File

@@ -127,10 +127,6 @@ Agent が閉じている間も実行する必要がある場合は、crontab、s
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```sh

View File

@@ -127,10 +127,6 @@ Use crontab, a systemd timer, or an external scheduler when jobs must run while
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -127,10 +127,6 @@ for job in fired:
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -17,6 +17,7 @@ import glob
import json
import os
import re
import shlex
import secrets
import subprocess
import threading
@@ -174,14 +175,191 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def request_permission(block, reason: str) -> str | None:

View File

@@ -418,10 +418,6 @@ Lead認証タスクの結果を受け取りました。残りの作業を調
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```sh

View File

@@ -418,10 +418,6 @@ The terminal exposes the user request, Lead's proposal, task state, claims, sele
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It
```sh

View File

@@ -415,10 +415,6 @@ Lead我已收到认证任务的结果接下来继续协调其余工作。
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -25,6 +25,7 @@ import json
import os
import random
import re
import shlex
import secrets
import select
import subprocess
@@ -1663,14 +1664,191 @@ TOOL_HANDLERS = {
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def register_hook(event: str, callback):

View File

@@ -169,10 +169,6 @@ lesson script を終了せず、model は次の turn で argument を修正で
---
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## 試してみる
```sh

View File

@@ -169,10 +169,6 @@ This chapter does not carry Task, Background, Cron, Team, or Worktree. They join
---
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Try It Out
```sh

View File

@@ -169,10 +169,6 @@ MCP error: TypeError: <lambda>() missing 1 required argument: 'query'
---
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 试一下
```sh

View File

@@ -25,6 +25,7 @@ Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
import glob
import os
import re
import shlex
import subprocess
from pathlib import Path
@@ -369,14 +370,191 @@ def assemble_system_prompt() -> str:
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
def register_hook(event: str, callback):

View File

@@ -222,10 +222,6 @@ command line から直接 Goal を設定することもできます。
python s17_goal_loop/code.py "/goal python -m pytest が exit code 0 で終了する"
```
## 継承する権限ルール
この章は s04 の permission hook を引き継ぐ。command の先頭または shell separator`;``&&``||``|``&`、括弧、改行)の直後にある完全な `rm`/`del` command word だけを大文字小文字を区別せず検出する。`model``delimiter``echo del test.txt` は危険な command として扱わない。
## s16 との関係
s16 は「複数の仕事をどう実行するか」を扱いました。どの step を並列化し、結果をどう検証し、中断後にどう resume するかを決めます。

View File

@@ -222,10 +222,6 @@ You can also set a Goal directly from the command line:
python s17_goal_loop/code.py "/goal python -m pytest exits with code 0"
```
## Inherited permission rule
This chapter carries forward the permission hook from s04. It recognizes `rm` and `del` case-insensitively only as complete command words at the start of a command or after a shell separator (`;`, `&&`, `||`, `|`, `&`, parentheses, or a newline). It does not treat `model`, `delimiter`, or `echo del test.txt` as destructive.
## Relationship to s16
s16 answers how a batch of work should run: which steps are concurrent, how results are verified, and how an interrupted run resumes.

View File

@@ -222,10 +222,6 @@ python s17_goal_loop/code.py
python s17_goal_loop/code.py "/goal python -m pytest 退出码为 0"
```
## 继承的权限规则
本章沿用 s04 的权限 hook只在命令开头或 shell 分隔符(`;``&&``||``|``&`、括号或换行)之后,按大小写不敏感方式识别完整的 `rm`/`del` 命令词。`model``delimiter``echo del test.txt` 不会被当成危险命令。
## 与 s16 的关系
s16 解决“一批工作怎样执行”:哪些步骤并行,结果怎样验证,失败后怎样恢复。

View File

@@ -32,6 +32,7 @@ import glob
import json
import os
import re
import shlex
import subprocess
import sys
import time
@@ -46,14 +47,191 @@ DEFAULT_STOP_HOOK_BLOCK_CAP = 8
MAX_GOAL_LENGTH = 4000
CLEAR_ALIASES = {"clear", "stop", "off", "reset", "none", "cancel"}
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
SHELL_SEPARATORS = ";&|\n"
DESTRUCTIVE_COMMANDS = {"rm", "del"}
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
COMMAND_PREFIXES = {"command", "call"}
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
MAX_COMMAND_NESTING = 16
DESTRUCTIVE_SUBCOMMAND = re.compile(
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
r"(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def shell_tokens(command: str) -> list[str]:
lexer = shlex.shlex(
command, posix=False, punctuation_chars=SHELL_SEPARATORS
)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
return list(lexer)
def shell_syntax_outside_single_quotes(command: str) -> str:
visible = []
single_quoted = double_quoted = escaped = False
for char in command:
if escaped:
visible.append(" ")
escaped = False
elif char == "\\" and not single_quoted:
visible.append(" ")
escaped = True
elif char == '"' and not single_quoted:
double_quoted = not double_quoted
visible.append(char)
elif char == "'" and not double_quoted:
single_quoted = not single_quoted
visible.append(" ")
else:
visible.append(" " if single_quoted else char)
return "".join(visible)
def unquote_shell_token(token: str) -> str:
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
return token[1:-1]
return token
def command_name(token: str) -> str:
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
if value.startswith("del/"):
return "del"
return value.replace("\\", "/").rsplit("/", 1)[-1]
def is_shell_separator(token: str) -> bool:
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
def is_shell_assignment(token: str) -> bool:
name, separator, _ = unquote_shell_token(token).partition("=")
return bool(
separator
and name
and not name[0].isdigit()
and name.replace("_", "a").isalnum()
)
def segment_has_destructive_command(
tokens: list[str], depth: int = 0
) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
index = 0
while index < len(tokens) and is_shell_assignment(tokens[index]):
index += 1
if index >= len(tokens):
return False
name = command_name(tokens[index])
if name in DESTRUCTIVE_COMMANDS:
return True
if name in CONTROL_PREFIXES:
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
if name == "env":
index += 1
while index < len(tokens) and (
unquote_shell_token(tokens[index]).startswith("-")
or is_shell_assignment(tokens[index])
):
index += 1
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in COMMAND_PREFIXES:
index += 1
options = []
while (
index < len(tokens)
and unquote_shell_token(tokens[index]).startswith("-")
):
options.append(unquote_shell_token(tokens[index]))
index += 1
if name == "command" and any(
"v" in option.lstrip("-").casefold() for option in options
):
return False
return segment_has_destructive_command(tokens[index:], depth + 1)
if name in SHELL_WRAPPERS:
for flag_index in range(index + 1, len(tokens)):
flag = unquote_shell_token(tokens[flag_index]).casefold()
is_command_flag = (
flag in {"/c", "/k"}
if name.startswith("cmd")
else flag.startswith("-")
and not flag.startswith("--")
and "c" in flag[1:]
)
if is_command_flag:
nested = " ".join(
unquote_shell_token(token)
for token in tokens[flag_index + 1:]
)
return contains_destructive_command(nested, depth + 1)
return False
if name == "if":
index += 1
while (
index < len(tokens)
and command_name(tokens[index]) in {"/i", "not"}
):
index += 1
if index >= len(tokens):
return False
condition = command_name(tokens[index])
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
return segment_has_destructive_command(
tokens[index + 2:], depth + 1
)
if "==" in unquote_shell_token(tokens[index]):
return segment_has_destructive_command(
tokens[index + 1:], depth + 1
)
if (
index + 2 < len(tokens)
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
):
return segment_has_destructive_command(
tokens[index + 3:], depth + 1
)
return False
if name == "for":
for do_index, token in enumerate(tokens[index + 1:], index + 1):
if command_name(token) == "do":
return segment_has_destructive_command(
tokens[do_index + 1:], depth + 1
)
return False
def contains_destructive_command(command: str, depth: int = 0) -> bool:
if depth >= MAX_COMMAND_NESTING:
return True
try:
tokens = shell_tokens(command)
except ValueError:
return True
if DESTRUCTIVE_SUBCOMMAND.search(
shell_syntax_outside_single_quotes(command)
):
return True
segment = []
for token in tokens:
if is_shell_separator(token):
if segment_has_destructive_command(segment, depth):
return True
segment = []
else:
segment.append(token)
return segment_has_destructive_command(segment, depth)
class GoalError(Exception):

View File

@@ -98,11 +98,38 @@ COMMAND_CASES = (
("echo ready & rm file.txt", True),
("(del file.txt)", True),
("rm; echo ready", True),
("if exist test.txt del test.txt", True),
("if not exist other.txt DEL test.txt", True),
("cmd /c del test.txt", True),
('cmd /c "if exist test.txt del test.txt"', True),
('for %F in (test.txt) do del "%F"', True),
("@DEL test.txt", True),
("call del test.txt", True),
("command rm test.txt", True),
("env FLAG=1 rm test.txt", True),
("sh -c 'rm test.txt'", True),
("bash -lc 'rm test.txt'", True),
("{ rm test.txt; }", True),
("/usr/bin/rm test.txt", True),
("del/q test.txt", True),
("echo $(rm test.txt)", True),
('echo "$(rm test.txt)"', True),
("echo `rm test.txt`", True),
("cat <(rm test.txt)", True),
("then " * 20 + "echo safe", True),
("echo 'unterminated", True),
("model list", False),
("delimiter file.txt", False),
("echo del file.txt", False),
("echo; delimiter file.txt", False),
("not-rm file.txt", False),
('echo "safe; del test.txt"', False),
("echo 'safe (rm test.txt)'", False),
('echo ";" del test.txt', False),
('if "del"=="safe" echo okay', False),
('printf "rm test.txt\\n"', False),
("command -v rm", False),
("echo '$(rm test.txt)'", False),
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long