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

发布时间:2026/9/7 17:50:12
gstack /careful:基于 Claude Code PreToolUse Hook 的破坏性命令护栏机制 gstack /careful基于 Claude Code PreToolUse Hook 的破坏性命令护栏机制【免费下载链接】gstackUse Garry Tans 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/gstackcareful/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-toolsBash、Read技能内允许的工具白名单hooks.PreToolUsematcher:Bash→bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh每次 Bash 工具调用前执行钩子脚本两个工程细节值得注意钩子命令锚定$HOME。test/hook-scripts.test.ts 中的 frontmatter hook command paths 用例专门断言careful/SKILL.md、freeze/SKILL.md、guard/SKILL.md等文件的command:行必须包含$HOME/.claude/skills/gstack/且绝不能引用CLAUDE_SKILL_DIR——因为 frontmatter 钩子在运行时变量就绪之前就会执行相对变量路径会静默解析失败护栏从此永远不触发。钩子是会话作用域session-scoped的。文档明确写道To deactivate, end the conversation or start a new one. 激活与退出都不需要卸载任何东西结束会话即解除全部防护。激活时文档还要求执行一段埋点脚本把{skill:careful,ts:...,repo:...}追加到~/.gstack/analytics/skill-usage.jsonl用于本地使用统计失败静默不影响护栏本身。二、保护清单MEDIUM 级破坏命令家族文档的 Whats protected 表格是护栏的完整基线实现与之一一对应模式示例风险源码内 pattern 名rm -rf/rm -r/rm --recursiverm -rf /var/data递归删除rm_recursiveDROP TABLE/DROP DATABASEDROP TABLE users;数据丢失drop_tableTRUNCATETRUNCATE orders;数据丢失truncategit push --force/-fgit push -f origin main历史改写git_force_pushgit reset --hardgit reset --hard HEAD~3未提交工作丢失git_reset_hardgit checkout ./git restore .git checkout .未提交工作丢失git_discardkubectl deletekubectl delete pod生产环境影响kubectl_deletedocker 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 的防御波次只匹配整条命令而非最后那个 rm。若只解析末段的 rmrm -rf / # rm -rf node_modules这种危险命令 注释伪装就会被末段的白名单后缀骗过锚定^...$使任何前缀、后缀、注释都落不进白名单。目标 token 排除(和反引号。rm -rf $(./wipe-all)/node_modules或反引号变体以白名单后缀结尾但括号内可以执行任意命令因此命令替换一律不能搭白名单的便车普通$VAR展开无括号仍放行。多行命令绝不进入白名单。case $CMD in *$\n*)直接让含换行的命令落入破坏性检查——因为 JSON 解析后 payload 里的\n是真实换行符rm -rf /\nrm -rf node_modules这种换行分隔、末行无害的攻击形态无法命中锚定白名单。结果就是源码注释所说的未知形态 fail closed落到破坏性检查测试 把rm -rf /; rm -rf node_modules、rm -rf / rm -rf node_modules、rm -rf / # rm -rf node_modules、rm -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 closedpayload 非空但解析不了时返回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_SIMPLE0整体落入 MEDIUM ask 家族——保守失败 询问绝不猜测。5.2 递归删除/、~、$HOME触发前提命令以可选sudorm开头且存在递归 flag长/短任意位置--no-preserve-root跟在目标后面也算。然后逐 token 判定跳过装饰 token选项、--、重定向2/dev/null是 Agent 生成命令最常见的后缀、后台符剥一层引号——rm -rf /与rm -rf /等价不能靠引号躲过拒绝每一个非选项 token 都必须是根类目标/、~、$HOME、/*等任何一个普通目标 token 出现即不算 HIGH全程set -fnoglob防止字面量/*在 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这种非根目标仍是 MEDIUMasktest/hook-scripts.test.ts。5.3 force-push 到默认分支判定链条分四步命令形如git push存在 force 语义-f/--force或plus-refspecmain、HEAD:main--force-with-lease被刻意排除——它是安全变体注释写明 never HIGH解析默认分支优先git symbolic-ref refs/remotes/origin/HEAD由于Conductor worktree 常常没有这个符号引用而 worktree 恰是 gstack 的主部署环境失败时回退探测refs/remotes/origin/main/refs/remotes/origin/master目标比对用定长字符串 token 比较而非把分支名插进正则注释正则元字符会过度/不足匹配带斜杠的默认分支如release/2.0保持完整main、HEAD: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体现了只增不减的两层保障加载时机这些文件只在八个内置家族全部未命中后才被读取所以无论文件内容是什么都无法抑制或弱化基线警告容错空行与#注释跳过无效 EREgrep返回码 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/dataaskreason 含 recursive deleterm -rf node_modules/rm -rf .next dist/rm -Rf node_modules无决策放行rm -rf /; rm -rf node_modules等 7 种安全伪装组合askrm -rf $(./wipe-all)/node_modules命令替换搭白名单askrm -R /denyreason 含 HIGHgit commit -m wip rm -rf /等 4 种引号截断形态ask#2426 回归非 JSON stdin / 非 JSON 载荷askfail closedrm${IFS}-rf${IFS}/、echo ... \| base64 -d \| shaskobfuscationpsql -c DROP TABLE users等带引号 SQLask无command字段的 payload、command: 42放行非 Bash 载荷ls -la、git status、npm 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...。组合姊妹技能 /guardfull 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 Tans 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),仅供参考