首页
/ gstack /careful:基于 Claude Code PreToolUse Hook 的破坏性命令护栏机制

gstack /careful:基于 Claude Code PreToolUse Hook 的破坏性命令护栏机制

2026-09-06 16:42:53作者:宣利权Counsellor

careful/SKILL.md 定义了 gstack 中的 /careful 技能——一套挂载在 Claude Code PreToolUse 钩子上的破坏性命令护栏。本文基于该文档及其配套实现 check-careful.sh 完整讲解它拦截哪些命令、两级(HIGH/MEDIUM)决策如何生效、如何 fail-closed 防御绕过,以及如何通过 careful-patterns.txt 做只增不减的项目级扩展,读完即可理解一个安全钩子从技能注册到逐字符校验的完整链路。

一、技能定位:在 Bash 执行前加一道人工确认

/careful 是 gstack 23 个技能中的安全防护技能,文档中的 "When to invoke this skill" 给出了触发场景:

  • 操作生产环境、调试线上系统、在共享环境中工作时;
  • 用户明确说出 "be careful"、"safety mode"、"prod mode"、"careful mode" 时。

它的行为契约是:激活后每一条 bash 命令在运行前都会被检查破坏性模式;一旦命中,Agent 会被警告(MEDIUM)或直接拒绝(HIGH),用户可以逐条覆盖 MEDIUM 警告后继续执行。

技能的完整注册信息见 careful/SKILL.md 的 frontmatter,各字段含义如下:

字段 取值 作用
name careful 技能名,用户以 /careful 调用
version 0.1.0 技能版本
description Safety guardrails for destructive commands 供技能路由/发现使用
triggers be careful / warn before destructive / safety mode 自然语言触发词
allowed-tools BashRead 技能内允许的工具白名单
hooks.PreToolUse matcher: Bashbash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh 每次 Bash 工具调用前执行钩子脚本

两个工程细节值得注意:

  1. 钩子命令锚定 $HOMEtest/hook-scripts.test.ts 中的 "frontmatter hook command paths" 用例专门断言 careful/SKILL.mdfreeze/SKILL.mdguard/SKILL.md 等文件的 command: 行必须包含 $HOME/.claude/skills/gstack/,且绝不能引用 CLAUDE_SKILL_DIR——因为 frontmatter 钩子在运行时变量就绪之前就会执行,相对变量路径会静默解析失败,护栏从此"永远不触发"。
  2. 钩子是会话作用域(session-scoped)的。文档明确写道:"To deactivate, end the conversation or start a new one." 激活与退出都不需要卸载任何东西,结束会话即解除全部防护。

激活时文档还要求执行一段埋点脚本,把 {"skill":"careful","ts":...,"repo":...} 追加到 ~/.gstack/analytics/skill-usage.jsonl,用于本地使用统计(失败静默,不影响护栏本身)。

二、保护清单:MEDIUM 级破坏命令家族

文档的 "What's protected" 表格是护栏的完整基线,实现与之一一对应:

模式 示例 风险 源码内 pattern 名
rm -rf / rm -r / rm --recursive rm -rf /var/data 递归删除 rm_recursive
DROP TABLE / DROP DATABASE DROP TABLE users; 数据丢失 drop_table
TRUNCATE TRUNCATE orders; 数据丢失 truncate
git push --force / -f git push -f origin main 历史改写 git_force_push
git reset --hard git reset --hard HEAD~3 未提交工作丢失 git_reset_hard
git checkout . / git restore . git checkout . 未提交工作丢失 git_discard
kubectl delete kubectl delete pod 生产环境影响 kubectl_delete
docker rm -f / docker system prune docker system prune -a 容器/镜像丢失 docker_destructive

check-careful.sh 中,这八个家族按顺序做 grep -qE 匹配,命中即停(后面的家族检查都带 [ -z "$WARN" ] 前置条件),每条都有固定的警告文案,例如:

  • rm -r 家族(注意正则 rm\s+(-[a-zA-Z]*[rR]|--recursive) 同时接受 BSD/macOS 的大写 -R 与 GNU 的 --recursive)→ "Destructive: recursive delete (rm -r). This permanently removes files."
  • SQL 家族在匹配前会把命令整体转小写(CMD_LOWER),因此 mysql -e drop database mydb 这类小写输入同样命中;
  • force-push 家族除了 -f/--force,还匹配 git 的 plus-refspec 语法git push origin +main)——这种写法无需任何 flag 即可强制推送,正则 (^|[[:space:]])\+[^[:space:]] 专门覆盖它。

三、安全例外:白名单 rm 的锚定式匹配

文档 "Safe exceptions" 一节列出不告警的构建产物清理:

rm -rf node_modules .next dist __pycache__ .cache build .turbo coverage

但"允许"的实现远比一句白名单苛刻。check-careful.sh 用一条锚定完整命令的正则来放行:

'^[[:space:]]*rm[[:space:]]+(-[a-zA-Z]*[rR][a-zA-Z]*[[:space:]]+|--recursive[[:space:]]+)(([^[:space:];&|#(`]*/)?(node_modules|\.next|dist|__pycache__|\.cache|build|\.turbo|coverage)[[:space:]]*)+$'

源码注释解释了这条正则背后的三道加固(对应 issue #2039 的防御波次):

  1. 只匹配整条命令,而非"最后那个 rm"。若只解析末段的 rm,rm -rf / # rm -rf node_modules 这种"危险命令 + 注释伪装"就会被末段的白名单后缀骗过;锚定 ^...$ 使任何前缀、后缀、注释都落不进白名单。
  2. 目标 token 排除 ( 和反引号rm -rf $(./wipe-all)/node_modules 或反引号变体以白名单后缀结尾,但括号内可以执行任意命令,因此命令替换一律不能"搭白名单的便车"(普通 $VAR 展开无括号,仍放行)。
  3. 多行命令绝不进入白名单case "$CMD" in *$'\n'*) 直接让含换行的命令落入破坏性检查——因为 JSON 解析后 payload 里的 \n 是真实换行符,rm -rf /\nrm -rf node_modules 这种"换行分隔、末行无害"的攻击形态无法命中锚定白名单。

结果就是源码注释所说的"未知形态 fail closed,落到破坏性检查":测试rm -rf /; rm -rf node_modulesrm -rf / && rm -rf node_modulesrm -rf / # rm -rf node_modulesrm -rf node_modules || rm -rf / 等一系列组合全部钉死为 ask;甚至 cd app && rm -rf node_modules 也会触发询问——注释里明确这是设计好的 fail-closed 假阳性:没有真正的 shell 解析器就无法区分"安全前缀 + 安全 rm"与"危险在前"的利用形态,所以宁可多问。

四、工作原理:PreToolUse 钩子的完整调用链

文档 "How it works" 一段概括了机制,check-careful.sh 与共享助手 hook-extract.sh 给出了全部细节。

4.1 输入与命令提取

Claude Code 在每次 Bash 工具调用前,把包含 tool_input 的 JSON 通过 stdin 传给钩子脚本。钩子需要从中取出 command 字段,这里历史上出过严重 bug:旧版提取器是 grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"'[^"]*第一个转义引号处截断,导致带引号参数的命令被截掉后半段——源码注释里列了三个真实失守样例:

git commit -m "wip" && rm -rf /   ->  提取到 'git commit -m '   -> 放行
bash -c "rm -rf /"                ->  提取到 'bash -c '        -> 放行
echo "x"; rm -rf ~                ->  提取到 'echo '            -> 放行

现在的提取逻辑在 hook-extract.shgstack_hook_extract_field 中:优先用 python3 -c 'json.loads(...)' 做真正的 JSON 解析,失败则回退到 node -e;解析失败(返回码 1)时调用方决定策略——careful 的策略是 fail closed:payload 非空但解析不了时,返回 ask 并提示"Could not parse the tool payload to safety-check this command. Approve only if you know what it does." 注释的定性很直接:"一个把守破坏性命令的钩子,不能因为读不懂输入就默认放行。"

而解析成功但没有 command 字段(非 Bash 工具的 payload)或 command 非字符串时,则输出空 {} 放行,保证钩子不误伤其他工具。test/hook-scripts.test.ts 的 "command extraction" 组用例分别钉住了这三种极性。

hook-extract.sh 是 careful 与 freeze 两个钩子共享的单一副本——注释记录了它存在的原因:两个钩子曾各带一份提取器,转义引号截断 bug 修在 careful 那份里时 freeze 的坏副本一直静默留着;共享之后"任何解析修复一次落地,构造上同时到达两个钩子"。

4.2 输出:hookSpecificOutput 信封

文档强调了一条 Claude Code 的隐性契约:决策必须嵌套在 hookSpecificOutput 之下,顶层的 permissionDecision 会被 Claude Code 直接忽略,警告等于没写。gstack_hook_decisionhook-extract.sh)统一生成三种结果之一:

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"[careful] ..."}}

permissionDecision 取值为 ask(警告,用户可覆盖)或 deny(拒绝,见下节 HIGH 级)。无命中时输出空对象 {}。理由文本由 gstack_hook_json_string 做 JSON 编码——注释特意警告"绝不能用 printf/sed 拼接钩子 JSON:路径里的引号或换行会造出畸形 JSON,而 Claude Code 对畸形决策的整个处理方式就是静默忽略——恰好在关键时刻 no-op 的 deny"。

4.3 Shell 混淆绊线

所有模式检查都是把命令当字符串匹配,但 bash 执行的是字符串展开之后的语义。check-careful.sh 因此设置了一条前置"绊线":命令中出现 ${IFS}/$IFS 分隔(rm${IFS}-rf${IFS}/ 能匹配 rm\s+ 之外的任何正则却执行完整的递归删除)、$(echo ... base64 ...) 展开、或 base64 -d | sh 管道到 shell 时,直接返回 ask 并提示 "Shell obfuscation detected (IFS word-splitting or base64-to-shell). Read the command carefully before approving."。源码的立场是"不与 bash 拼解析能力"——这些分裂/解码原语在人类真正想无人值守执行的命令中极其罕见,一律要求人工过目。测试 同时验证了 rm${IFS}-rf${IFS}/echo cm0gLXJmIC8= | base64 -d | sh 会触发询问,而 cat file.b64 | base64 -d > out.bin 这类普通 base64 解码不受影响。

4.4 遥测

每次命中(无论 ask/deny)都会调用 gstack_hook_log_fire 追加一条 {"event":"hook_fire","skill":"careful","pattern":...,"ts":...,"repo":...} 记录——只记 pattern 名,从不记录命令内容;且目录取自 GSTACK_HOME 环境变量,让测试不会污染操作者真实的 analytics 文件;写日志失败也是 best-effort,绝不影响钩子决策本身。

五、HIGH 级:两种"灾难形态"直接拒绝

文档 "HIGH tier (hard deny)" 一节是最值得精读的部分:在可覆盖的 MEDIUM 警告之上,还有两种形态是 deny 而不是 ask,且实现(check-careful.sh)对边界做了大量推敲。

5.1 仅 SIMPLE 命令有资格进入 HIGH

字符串匹配无法回答复合命令"到底做什么"(cd X && git push --force——哪个 cwd?哪个仓库?),所以钩子先用 case 检查命令是否含 ;&&||| 或换行;含任一则 _IS_SIMPLE=0整体落入 MEDIUM ask 家族——"保守失败 = 询问,绝不猜测"。

5.2 递归删除 /~$HOME

触发前提:命令以(可选 sudo +)rm 开头,且存在递归 flag(长/短任意位置,--no-preserve-root 跟在目标后面也算)。然后逐 token 判定:

  • 跳过装饰 token:选项、--、重定向(2>/dev/null 是 Agent 生成命令最常见的后缀)、后台符;
  • 剥一层引号——rm -rf "/"rm -rf / 等价,不能靠引号躲过拒绝;
  • 每一个非选项 token 都必须是根类目标(/~$HOME/* 等),任何一个普通目标 token 出现即不算 HIGH;
  • 全程 set -f(noglob),防止字面量 /* 在 word-splitting 时被 shell 展开。

命中则输出:[careful][HIGH] Recursive delete of / or the home directory is blocked while /careful is active. If you truly mean it, end the /careful session first. 测试钉住了 rm -R /deny(大写 -R 是 BSD/macOS 的递归标志,早期正则只认小写 rrm -R / 曾静默放行),以及 rm -fR /home/user 这种非根目标仍是 MEDIUM asktest/hook-scripts.test.ts)。

5.3 force-push 到默认分支

判定链条分四步:

  1. 命令形如 git push
  2. 存在 force 语义:-f/--force plus-refspec(+main+HEAD:main);--force-with-lease 被刻意排除——它是安全变体,注释写明 "never HIGH";
  3. 解析默认分支:优先 git symbolic-ref refs/remotes/origin/HEAD;由于 Conductor worktree 常常没有这个符号引用(而 worktree 恰是 gstack 的主部署环境),失败时回退探测 refs/remotes/origin/main / refs/remotes/origin/master
  4. 目标比对用定长字符串 token 比较而非把分支名插进正则(注释:正则元字符会过度/不足匹配),带斜杠的默认分支如 release/2.0 保持完整;+mainHEAD:main 等 refspec 会先剥 + 和前缀再比较。另有特例:裸 git push --force(只有 force flag、无远端/ref)指向当前分支的 upstream,仅当当前分支就在默认分支上时才算 HIGH。

测试用临时 git 仓库把默认分支钉成 trunk(见 withGitRepo),确保 git push --force origin main 在非默认目标下仍是 MEDIUM ask 而不是误伤。

5.4 定位:建议性的硬停,不是策略边界

文档对 HIGH 级的自我定性必须原样继承:"A best-effort advisory hard-stop, not a policy boundary: the escape hatch is ending the opt-in, session-scoped /careful session." 即:它尽力拦住两种灾难,但逃生门是结束这个用户主动开启的会话——真正的强制策略边界需要企业级管控,/careful 明确不扮演那个角色。

六、项目级自定义模式:只能加,不能减

文档 "Project patterns (additive only)" 允许在两个位置追加告警规则,每行一条 POSIX ERE,支持 # 注释:

  • 全局:~/.gstack/careful-patterns.txt
  • 按项目:~/.gstack/projects/<slug>/careful-patterns.txt

实现(check-careful.sh)体现了"只增不减"的两层保障:

  1. 加载时机:这些文件只在八个内置家族全部未命中后才被读取,所以无论文件内容是什么,都无法抑制或弱化基线警告;
  2. 容错:空行与 # 注释跳过;无效 ERE(grep 返回码 2)跳过该行为止——"钩子绝不能因为配置里的一个拼写错误而崩掉"。

性能上有一个值得注意的短路:解析项目 slug 需要一次子进程 + git 调用,而钩子对每条 Bash 命令都要跑,所以先 find ... -name careful-patterns.txt -print -quit 探测是否存在任何按项目的模式文件,存在才去执行 bin/gstack-slug 解析 slug,把常态开销降到零。

GSTACK_HOME 环境变量可整体重定向状态目录(默认 ~/.gstack),测试正是靠它把模式文件与 analytics 都关进沙箱。

七、测试如何锁住这套护栏

test/hook-scripts.test.tsspawnSync('bash', [check-careful.sh]) + JSON stdin 的方式对钩子做端到端断言,覆盖矩阵与文档逐条对应,且包含大量"攻击形态"回归用例:

用例 期望
rm -rf /var/data ask,reason 含 "recursive delete"
rm -rf node_modules / rm -rf .next dist / rm -Rf node_modules 无决策(放行)
rm -rf /; rm -rf node_modules 等 7 种"安全伪装"组合 ask
rm -rf $(./wipe-all)/node_modules(命令替换搭白名单) ask
rm -R / deny,reason 含 "HIGH"
git commit -m "wip" && rm -rf / 等 4 种引号截断形态 ask(#2426 回归)
非 JSON stdin / 非 JSON 载荷 ask(fail closed)
rm${IFS}-rf${IFS}/echo ... | base64 -d | sh ask(obfuscation)
psql -c "DROP TABLE users" 等带引号 SQL ask
command 字段的 payload、command: 42 放行(非 Bash 载荷)
ls -lagit statusnpm install 放行

这套测试的意义在于:护栏的每一条"保守失败"决策都是被用例显式钉住的设计意图(例如 "A future per-segment parser must consciously change this test"),防止后续优化在"消除假阳性"的名义下悄悄打开 fail-closed 的缺口。

八、启用、组合与退出

  • 启用:安装 gstack 后(setup 脚本会把 careful/bin 等全部运行资产装到技能目录,见 setup 中关于 "installs every runtime asset a skill ships" 的排除式清单),对 Claude Code 说 "be careful" / "safety mode" / "prod mode",或由路由匹配 /careful。frontmatter 中的 hooks 随即注册 PreToolUse 检查,状态消息为 "Checking for destructive commands..."。
  • 组合:姊妹技能 /guard("full safety mode")直接复用 careful/bin/check-careful.sh 的 Bash 钩子,再叠加 /freeze 的 Edit/Write 目录边界钩子,实现"破坏命令告警 + 编辑范围锁定"的最大安全组合;二者由同一安装流程一起部署。
  • 退出:结束当前会话或开启新会话即可——钩子是会话作用域的,不存在需要清理的持久状态。

小结

/careful 的价值不在模式表本身,而在于它展示了一个 AI Agent 安全钩子应有的工程姿态:真实的 JSON 解析而非 grep 取字段、锚定式白名单、对混淆原语的绊线、fail-closed 的输入极性、只增不减的用户扩展、以及对"这是建议性硬停而非策略边界"的诚实定位。这些取舍大多能从 careful/SKILL.mdcareful/bin/check-careful.shcareful/bin/hook-extract.shtest/hook-scripts.test.ts 的注释和用例中逐条找到依据,适合作为 Agent 工具链安全设计的参考样本。

登录后查看全文
热门项目推荐
相关项目推荐