
前言
最近写 Agent 相关配置时,我一直在想一件事:我们到底该怎么相信 AI 会遵守项目规则?
很多项目里都有类似的说明:不要改生产配置、不要碰 .env、不要执行删除命令。我们会把它写进 CLAUDE.md、AGENTS.md,或者 Cursor Rules。平时看起来没问题,Agent 也大多数时候会照做。
但有一天,它真的改了 config/production/ 里的连接串,你才会发现:这类规则写得再清楚,也只是上下文,不是权限。
这篇文章不讲怎么把 prompt 写得更长。我们直接解决一个更实际的问题:怎样让 Agent 在动手之前被拦下来。
本文你能学到什么
看完这篇文章,你应该能搞清楚三件事:
- 为什么
CLAUDE.md和 Rules 不能当安全边界; - 怎样拦住 AI 修改
config/production/或执行明显危险的删除命令; - Claude Code、Codex、OpenCode、Cursor 分别应该怎么配。
不会也没关系,下面所有例子都围绕同一个小场景展开,直接替换路径就能放进自己的项目。
先看一个很容易忽略的问题
假设你写了这样一段规则:
## Production boundary
Never modify files under config/production/.
Never run destructive commands in this repository.
Ask for human confirmation before any production operation.
这段话当然应该保留。它能让 Agent 理解项目边界,也能减少无谓的尝试。
但这里有的小伙伴可能会问:既然写了 Never,它为什么还可能不遵守?
因为语言模型不是规则引擎。它每一次响应都在根据当前上下文生成“最合适”的下一步;任务越复杂、上下文越长,规则越可能被别的信息稀释或误解。文件写入和 Shell 命令却不是概率事件,一旦执行,副作用就已经发生了。
所以我们在项目里最好分两层处理:
| 这一层 | 解决什么问题 | 放在哪里 |
|---|---|---|
| 软规则 | 告诉 Agent 应该怎样做 | CLAUDE.md、AGENTS.md、Cursor Rules |
| 硬护栏 | 在操作发生前允许或拒绝 | Hook、权限、Sandbox、CI |
软规则负责“讲道理”,硬护栏负责“关门”。生产边界必须放在第二层。
好了,开始我们的正文学习吧。
Claude Code:用 PreToolUse 在执行前拒绝
Claude Code 的 PreToolUse Hook 会在工具参数生成后、真正执行前运行。Hook 输出 deny,这次工具调用就不会继续。
先创建 .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-production.sh"
}
]
}
]
}
}
再创建 .claude/hooks/guard-production.sh:
#!/usr/bin/env bash
set -euo pipefail
input="$(cat)"
tool="$(jq -r '.tool_name' <<<"$input")"
if [[ "$tool" == "Bash" ]]; then
value="$(jq -r '.tool_input.command // ""' <<<"$input")"
else
value="$(jq -r '.tool_input.file_path // ""' <<<"$input")"
fi
if [[ "$value" == *"config/production/"* ]] || [[ "$value" =~ (^|[[:space:];|&])rm[[:space:]]+-[[:alpha:]]*r[[:alpha:]]*f ]]; then
jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"Production boundary: operation blocked."}}'
fi
最后执行:
chmod +x .claude/hooks/guard-production.sh
这段脚本做的事情很简单:Bash 取命令文本,编辑工具取目标文件路径;只要碰到生产目录或 rm -rf,就拒绝。
不过别把它理解成万能安全脚本。find -delete、Python、Node 也能删文件。这个 Hook 的价值是把已知高风险操作提前挡住;更底层的文件权限和 Sandbox 还是要开。Claude Code 也支持用 filesystem.denyRead 和 credentials.envVars 保护密钥与凭证。
Codex:项目 .codex/config.toml 也可以挂 Hook
Codex 现在同样支持生命周期 Hook。对于文件编辑和命令执行,最常用的还是 PreToolUse。
在项目根目录新建 .codex/config.toml:
[[hooks.PreToolUse]]
matcher = "^(Bash|apply_patch)$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = 'python3 .codex/hooks/guard_production.py'
timeout = 5
statusMessage = "Checking production boundary"
然后新建 .codex/hooks/guard_production.py:
#!/usr/bin/env python3
import json, re, sys
event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")
blocked = (
"config/production/" in command
or re.search(r"(^|[\s;|&])rm\s+-[a-zA-Z]*r[a-zA-Z]*f", command)
)
if blocked:
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Production boundary: operation blocked."
}}))
Codex 的 apply_patch 会把补丁内容放进 tool_input.command,所以这一个脚本能够同时检查补丁和 Bash 命令。首次启用项目 Hook 时,可以在 Codex 里用 /hooks 审阅并信任它。
这里还要补一句:项目 Hook 是开发团队的本地护栏,不是企业级的最终权限系统。生产身份、云密钥、部署权限仍然应该放在独立 IAM、CI 和受管策略里。Hook 拦的是“这次操作”,权限系统决定“它本来有没有资格操作”。
OpenCode:用插件的 tool.execute.before
OpenCode 的思路不太一样,它更像是在 Agent 运行时插一个插件。把下面代码放在 .opencode/plugins/guard-production.ts:
import type { Plugin } from "@opencode-ai/plugin";
export const GuardProduction: Plugin = async () => ({
"tool.execute.before": async (input, output) => {
const raw = JSON.stringify(output.args ?? {});
const dangerousRm = /(^|[\s;|&])rm\s+-[a-zA-Z]*r[a-zA-Z]*f/.test(raw);
const writesProduction =
["write", "edit", "apply_patch"].includes(input.tool) &&
raw.includes("config/production/");
if (dangerousRm || writesProduction) {
throw new Error("Production boundary: operation blocked.");
}
},
});
OpenCode 会加载 .opencode/plugins/ 下的本地插件。这里的关键点只有一个:tool.execute.before 是在内建工具真正执行前触发的,抛错就能把调用拦住。
另外,OpenCode 的 edit 权限覆盖 edit、write 和 apply_patch。有的小伙伴只会盯着 write,结果补丁工具还是能改文件;这也是为什么例子里同时检查了三类写操作。
Cursor:Rules 不等于 Hook,先把权限收紧
Cursor 的 .cursor/rules/*.mdc 很适合放项目约定,但它和 CLAUDE.md 一样,解决的是“让模型知道规则”,不是“强制执行规则”。
Cursor 已经发布了 Beta Hooks,官方确认它可以用于审计、拦截命令和上下文脱敏。不过截至本文写作,官方公开文档还没有提供一个稳定、可复现的项目级 Hook 配置格式。
所以 Cursor 这里我不建议贴一段来历不明的 hooks.json。更实用的做法是:
- 在
.cursor/rules/production.mdc写清生产边界; - 关闭自动执行,保留终端命令审批;
- 开启 Sandbox,只给工作区和必要的网络域名;
- 如果团队已经拿到 Hooks Beta,就按当前 Cursor 设置页生成的格式接入相同的检查逻辑;
- 不要在非交互 CLI 模式下把生产目录和部署凭证暴露给 Agent。
这部分配置看起来没那么“自动化”,但安全配置最怕伪确定性。一个版本已经失效的 Hook 配置,远不如一条仍然会弹出来的审批可靠。
最后再说三个容易踩的坑
第一,黑名单不是权限系统。 rm -rf 只是一个例子。不要试图列完所有危险命令,应该同时限制路径、目录权限、网络出口和生产身份。
第二,PostToolUse 不能回滚副作用。 它适合跑 lint、记审计日志、提醒 Agent 修复问题;但文件已经写了、命令已经跑了,再“阻止”已经晚了。真正的禁止要写在 PreToolUse 或 tool.execute.before。
第三,不要让密钥本来就暴露在 Agent 面前。 Hook 可以降低误操作概率,但它不是保险柜。.env、云凭证、SSH 私钥、生产数据库连接,都应该配合最小权限、短期凭证和隔离环境处理。
总结
很多开发者已经把项目规则写得很详细了,但这不代表规则一定会被 100% 执行。
CLAUDE.md、AGENTS.md、Cursor Rules 都有用,它们负责让 Agent 理解项目。真正不能出错的事情,则要在工具调用之前再加一道 Hook,并把最底层的权限交给 Sandbox、CI 和云 IAM。
如果你今天只想做一件事,我建议先选一个你绝对不能接受的操作,例如“不能改生产配置”,为它写一个 PreToolUse 拦截器,再故意测试一次违规操作。
因为没测过的护栏,很多时候也只是一段写得很认真的说明文档。
参考资料
- https://code.claude.com/docs/en/hooks
- https://code.claude.com/docs/en/settings
- https://learn.chatgpt.com/docs/hooks
- https://learn.chatgpt.com/docs/config-file/config-advanced
- https://dev.opencode.ai/docs/plugins/
- https://dev.opencode.ai/docs/tools/
- https://cursor.com/changelog/1-7
- https://docs.cursor.com/en/cli/using