gstack /careful:基于 Claude Code PreToolUse Hook 的破坏性命令护栏机制
2026/9/7 17:50:07 网站建设 项目流程

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

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

careful/SKILL.md定义了 gstack 中的/careful技能——一套挂载在 Claude CodePreToolUse钩子上的破坏性命令护栏。本文基于该文档及其配套实现 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,各字段含义如下:

字段取值作用
namecareful技能名,用户以/careful调用
version0.1.0技能版本
descriptionSafety guardrails for destructive commands供技能路由/发现使用
triggersbe careful/warn before destructive/safety mode自然语言触发词
allowed-toolsBashRead技能内允许的工具白名单
hooks.PreToolUsematcher:Bashbash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh每次 Bash 工具调用前执行钩子脚本

两个工程细节值得注意:

  1. 钩子命令锚定$HOME。test/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 --recursiverm -rf /var/data递归删除rm_recursive
DROP TABLE/DROP DATABASEDROP TABLE users;数据丢失drop_table
TRUNCATETRUNCATE orders;数据丢失truncate
git push --force/-fgit push -f origin main历史改写git_force_push
git reset --hardgit reset --hard HEAD~3未提交工作丢失git_reset_hard
git checkout ./git restore .git checkout .未提交工作丢失git_discard
kubectl deletekubectl delete pod生产环境影响kubectl_delete
docker rm -f/docker system prunedocker 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.sh 的gstack_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_decision(hook-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这种非根目标仍是 MEDIUMask(test/hook-scripts.test.ts)。

5.3 force-push 到默认分支

判定链条分四步:

  1. 命令形如git push
  2. 存在 force 语义:-f/--forceplus-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在非默认目标下仍是 MEDIUMask而不是误伤。

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.ts 以spawnSync('bash', [check-careful.sh])+ JSON stdin 的方式对钩子做端到端断言,覆盖矩阵与文档逐条对应,且包含大量"攻击形态"回归用例:

用例期望
rm -rf /var/dataask,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 \| shask(obfuscation)
psql -c "DROP TABLE users"等带引号 SQLask
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.md、careful/bin/check-careful.sh、careful/bin/hook-extract.sh 与 test/hook-scripts.test.ts 的注释和用例中逐条找到依据,适合作为 Agent 工具链安全设计的参考样本。

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询