跳至内容

额外内容 — 治理钩子

📎 额外实验 — 不属于 Copilot SDK。 此内容介绍由 .github/hooks/ 驱动的 Shell 钩子,这是一个 Copilot CLI 功能。SDK 提供了对应的进程内机制,详见实验 03 — 工具中的权限部分。本实验为选修内容,并且独立于编号的 SDK 技术路线。

目标: 验证 preToolUse 安全检查关卡确实能够阻止对机密文件的访问,查看审计日志记录的活动,并了解为什么配置错误的钩子比完全没有钩子更危险。

时间: 约 20 分钟

先决条件: 额外内容 — 自定义代理 完成。 jq 已安装 — 钩子脚本依赖它。

步骤 1 — 查看钩子的连接方式

cat .github/hooks/retail-governance.json
ls -l .github/hooks/scripts/

四个生命周期事件,各自对应一个脚本:

事件 脚本 用途
sessionStart session-init.sh 声明原则并开始审计轨迹
preToolUse security-gate.sh 允许或拒绝 工具调用执行前
postToolUse audit-logger.sh 记录实际发生的情况
sessionEnd session-end.sh 结束会话并生成摘要

每个条目都会声明 type、要执行的 bash 命令、工作目录和 timeoutSec

只有 preToolUse 可以 停止 所有操作。其他事件仅负责观察。

步骤 2 — 一个警示案例

此仓库曾有一段时间包含故障的防护关卡。设置指向:

./.github/hooks/scripts/security-gate-notworking.sh

…但磁盘上的文件是 security-gate.sh。参照的脚本 并不存在.

安全检查关卡没有报错,也没有发出警告;它只是完全没有运行。每个工具调用都未经检查便直接通过,但仓库看起来却像是受到了完整治理。旧版的 .env 甚至包含注释,声明‘preToolUse 钩子应封锁对此文件的访问’。实际上并不会。

这个问题已修正,也是本实验中最重要的教训:

⚠️ 缺乏警告的错误配置比完全没有配置更糟,因为这会制造虚假的信心。请务必证明安全检查关卡确实有触发。

确认参照现在正确无误:

grep -o '"bash": "[^"]*"' .github/hooks/retail-governance.json

列出的每个路径都必须存在于 .github/hooks/scripts/.

步骤 3 — 阅读安全检查关卡

cat .github/hooks/scripts/security-gate.sh

合约很简单 — stdin 输入 JSON,stdout 输出决策:

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')

它会在三种情况下拒绝:

  1. 具有破坏性的 Bash 操作rm -rf /, rm -rf ., DROP TABLE, DROP DATABASE, format, mkfs.、Fork bomb 模式
  2. 机密信息访问 — 提到以下内容的命令: .env, credentials, secrets, .pem, .keypassword
  3. 越界写入edit/create 之外 src/, tests/, docs/.github/

并发出下列其中一个结果:

{"permissionDecision":"allow"}
{"permissionDecision":"deny","permissionDecisionReason":"..."}

⚠️ 请注意,即使拒绝请求,脚本仍会以状态码 0 退出。决策通过 JSON 负载传递,而不是通过退出代码传递;非零退出代码表示钩子运行失败,而不是有意拒绝请求。

步骤 4 — 证明安全检查关卡会封锁机密信息

确认脚本具备执行权限,且记录目录已存在:

chmod +x .github/hooks/scripts/*.sh
mkdir -p logs

现在,请通过管道传入真正工具调用会发送的 JSON,直接进行测试:

echo '{"toolName":"bash","toolArgs":{"command":"cat .env"}}' \
  | ./.github/hooks/scripts/security-gate.sh

预期结果:

{"permissionDecision":"deny","permissionDecisionReason":"Access to credential/secret files blocked by security policy"}

这意味着安全检查关卡确实生效 — 即先前从未执行的同一项检查。

步骤 5 — 测试其他路径

具有破坏性的命令:

echo '{"toolName":"bash","toolArgs":{"command":"rm -rf /"}}' \
  | ./.github/hooks/scripts/security-gate.sh

"Destructive command blocked by retail governance policy"

写入允许目录之外的位置:

echo '{"toolName":"create","toolArgs":{"path":"/etc/hosts"}}' \
  | ./.github/hooks/scripts/security-gate.sh

"File edits restricted to src/, tests/, docs/, .github/, and top-level project docs"

仓库根目录中的项目文件应当允许写入:

echo '{"toolName": "create", "toolArgs": {"path": "README.md"}}' \
  | ./.github/hooks/scripts/security-gate.sh

{"permissionDecision":"allow"}

允许列表会匹配相对于仓库的路径,因此仓库外即使存在名称相似的文件,也仍会被拒绝:

echo '{"toolName": "create", "toolArgs": {"path": "/etc/README.md"}}' \
  | ./.github/hooks/scripts/security-gate.sh

→ 遭到拒绝。若只比对文件名,原本会让该操作通过

以及必须通过的合法调用:

echo '{"toolName":"bash","toolArgs":{"command":"dotnet build"}}' \
  | ./.github/hooks/scripts/security-gate.sh

{"permissionDecision":"allow"}

⚠️ 务必同时测试允许案例。 一个拒绝所有操作的安全检查关卡虽然能通过所有“是否阻止了操作”的测试,却会让仓库完全无法使用。

步骤 6 — 查看拒绝记录

cat logs/security-denials.log

每次拒绝都会附上 UTC 时间戳、工具及原因,并附加至日志:

2026-08-10T11:04:35Z DENIED tool=bash reason="Access to credential/secret files blocked by security policy"

这正是合规审查人员需要的产物:既证明控制措施确实存在,也证明它确实被触发。

步骤 7 — 安全检查关卡不需要文件实际存在

此仓库没有提交任何机密文件;.env 已被 gitignore 忽略,也绝不应提交。这不会削弱安全检查关卡,因为它匹配的是 命令文字,而不是磁盘上实际存在的内容。

确认没有 .env,然后仍尝试读取它:

ls .env 2>/dev/null || echo "no .env present"
echo '{"toolName":"bash","toolArgs":{"command":"cat .env"}}' \
  | ./.github/hooks/scripts/security-gate.sh

仍然遭到拒绝。其他机密信息模式也会得到相同结果:

echo '{"toolName":"bash","toolArgs":{"command":"cat ~/.ssh/id_rsa.key"}}' \
  | ./.github/hooks/scripts/security-gate.sh

💡 这是双刃剑。文本比对表示安全检查关卡不会因文件尚不存在而被绕过 — 但避免使用触发词的命令也可能绕过它(cat .en''v,或通过脚本读取文件)。请将它视为防止意外的防护措施,而非抵御蓄意攻击者的防线。

步骤 8 — 在触发钩子时执行会话

copilot -p "List the files in the src directory" --allow-all-tools

接着检查审计日志提取的内容:

ls -la logs/
tail -20 logs/*.jsonl 2>/dev/null || tail -20 logs/*.log 2>/dev/null

💡 CLI 会为每个会话配置钩子。如果没有显示任何内容,请确认 CLI 已为当前仓库加载 .github/hooks/retail-governance.json

步骤 9 — 禁用钩子

开发钩子时,您会希望暂时停用它。您可以重新命名设置:

mv .github/hooks/retail-governance.json .github/hooks/retail-governance.json.off
# restore with the reverse

也可以从数组中删除某个事件条目,以暂时禁用该事件。

⚠️ 切勿在共享分支中停用安全检查关卡后忘记恢复,否则会重现步骤 2 中的静默失败。建议添加 CI 检查,验证配置中每个 bash 命令引用的路径都确实存在于磁盘上。

✅ 检查点

  • [x] 您能说出四个钩子事件,以及哪一个可以拒绝操作
  • [x] 您已证明安全检查关卡会封锁机密信息、破坏性命令及不当位置的写入
  • [x] 您已确认合法调用仍会通过
  • [x] 您已找到拒绝记录中的证据
  • [x] 您能说明故障的参照为何危险

💡 加分练习

security-gate.sh 中添加规则,阻止对 main 执行 git push --force。请验证它会拒绝强制推送,同时确认普通的 git push 仍然可以通过。