diff --git a/.claude/hooks/block-git.py b/.claude/hooks/block-git.py index 097969f1..4203d318 100644 --- a/.claude/hooks/block-git.py +++ b/.claude/hooks/block-git.py @@ -1,16 +1,142 @@ #!/usr/bin/env python3 -"""铁律 #2 机械执行:Bash 命令以 git 开头 → 拒绝执行(exit 2)。""" +"""铁律 #2 机械执行:命令串中出现 git **调用** → 拒绝执行(exit 2)。 + +加固(2026-09-16,TODO #2026-09-11-7):原实现只拦「整条命令以 git 开头」, +`cd x && git …` / `bash -c 'git …'` / `… | git …` / `$(git …)` 全能绕过。 + +现覆盖: + · 以 git 开头(原行为;含 `/usr/bin/git` 等带路径形态) + · 分隔符之后:`;` `&&` `||` `|` `&` `(` `)` 换行 + · 命令替换 / 子壳:`$(git …)`、反引号、`(git …)` + · shell 内联脚本:`bash -c '…'` / `sh -c "…"`(递归扫描,深度 ≤ 3) + · 前缀命令之后:sudo / env / nice / nohup / time / exec / command / xargs / + timeout / stdbuf / setsid / ionice / cpulimit / watch(含其旗标与数字参数) + +**不得误伤(同为零容忍:误伤会让人绕开 hook)**: + · `jj git push`(git 不是命令词) + · **引号内**的 git 文本:`jj commit -m '… git log …'` · `echo "use git"` + · **heredoc 体内**的 git 文本(`<<'PY' … PY`;脚本体是数据不是命令) + · `grep -n git` · `find . -name .gitignore` · `cat .gitignore` + +实现要点:先剥离 heredoc 体(按行扫,遇 `<<[-]?WORD` 丢弃到终止行), +再用 `shlex`(`punctuation_chars`,**尊重引号**)分词,按分隔符 token 切段, +每段跳过前导「环境赋值 / 前缀命令 / shell 名 / 旗标 / 纯数字」后看**首个命令词**。 + +失败模式:hook 自身异常 **一律放行**(exit 0)—— 宁可漏拦,不可把会话卡死。 +""" import json +import re +import shlex import sys -data = json.load(sys.stdin) -cmd = data.get("tool_input", {}).get("command", "") -stripped = cmd.strip() - -if stripped == "git" or stripped.startswith("git "): - sys.stderr.write( - "铁律 #2:全面使用 jj,禁止 git。\n" - "改用 jj 命令:jj status / jj log / jj describe / jj git push / jj bookmark ...\n" - ) - sys.exit(2) # PreToolUse exit 2 = block the tool call -sys.exit(0) +PREFIX_CMDS = { + "sudo", "doas", "env", "nice", "nohup", "time", "exec", "command", "xargs", + "timeout", "stdbuf", "setsid", "ionice", "cpulimit", "parallel", "watch", +} +SHELLS = {"bash", "sh", "zsh", "dash", "ksh", "fish"} +MAX_DEPTH = 3 +PUNCT = "();|&<>`$\n" +SEP_CHARS = set("();|&`$\n") # 切段用;< > 是重定向、不是命令分隔符 +ENV_ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=") +HEREDOC_RE = re.compile(r"<<-?\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\1") +INNER_RE = re.compile( + r"""(?:^|[;&|(\s])(?:bash|sh|zsh|dash|ksh|fish)\s+(?:-\S+\s+)*-\S*c\S*\s+""" + r"""(?P['"])(?P