别急着夸 Gemini 的新钩子,先看你的 deny 是不是空的

cover

哈喽,我是飞飞。

Google 前几天给 Gemini 的 Managed Agents 加了个功能,叫环境钩子。

先撇清一下,Managed Agents 这东西我在 I/O 那阵写过一次,讲的是它把沙箱、工具、浏览器打包成一次 API 调用。今天这篇跟那个没关系,只盯新加的这一样。

环境钩子干的事,一句话说清:在 agent 每次调用工具的前后,插一段你自己的脚本。官方点名了两个用途,安全审查和代码格式化。

我看到这两个词的时候乐了一下。因为两周前我刚写过一篇文章,讲的就是怎么给 Claude Code 配这种东西。

然后我干了件事,打开自己的配置看了一眼。

看完有点想笑,也有点笑不出来。

它给的是能拦的闸

先把这个功能讲准,尤其是最要紧的那一点。

挂载点有两个,一个卡在工具执行之前,一个卡在执行之后。配置写在一个叫 hooks.json 的文件里,脚本跑在远程沙箱内部。

真正决定这功能值不值钱的,是下面这条。

你的脚本可以返回一个 deny,附上理由。这时候那次工具调用会被直接跳过,而且拒绝的理由会被送进模型的上下文里

这一下就把它跟「只能看不能拦」的监控区分开了。它不只是在旁边记账,能真把手伸出去按住。

另外还有个预算上限,设一个最大 token 数,到顶了交互会返回一个未完成状态,你可以拿上次的 interaction id 接着跑。免费套餐也开了,不过官方只说了无账单的项目能试,没给具体额度,那我就不编数字。cron 定时触发也提了一句,粒度没说。

说明一下,这些我都没上手跑过,是读完官方文档之后的判断,不是实测报告。

两周前我写过一模一样的东西

现在说说我为什么乐。

7 月 14 号我写过一篇,讲 hook。当时我给自己配了三样。

一个是类型检查。我在 CLAUDE.md 里写过「改完代码跑一遍类型检查」,但那是建议式的,长会话上下文一压缩它转头就忘。后来我换成 hook,编辑完文件自动跑检查,报错就用 exit 2 把这步堵死。我当时的原话是:exit 2 这个返回码是命门,它不是提个醒,是真把这步堵死,Claude 不修好就过不去。

一个是格式化,三行配置,它一动文件就自动跑 Prettier。

还有一个是防手贱。我卡在它执行命令之前,配上 permissions.deny,把带通配符的删除、强制推送这种一锤子买卖的命令直接拦掉。原话是:它再自信,这道闸也得先问过我。

那篇文章我还总结了一句自认为挺精辟的分层:CLAUDE.md 管记忆,skill 管流程,hook 管纪律。

你看,Google 这次给的东西,跟我两周前手搓的那套是一回事。执行前后各一个挂载点,能返回拒绝,能拦住。我用 exit 2,它用 deny,形状一样。

区别在于,我那套是自己一条条抠出来的土法,只在 Claude Code 里有效。它把这件事做成了标准件,写在一个配置文件里,跑在自己的沙箱内。

而且有一点它做得比我好。我的 hook 拦下来之后,得我自己把错误甩回去让模型重来。它是拒绝理由自动进上下文,模型当场就知道自己为什么被拦。这个差别不大,但少一道手工活。

我打开配置一看

好,说回我为什么又笑不出来。

写完那篇文章两周,今天我打开自己的配置文件看了看。查了两个地方,项目里的一个,全局的一个。

我文章里推荐的那三样,一个都不在

类型检查的 hook,没有。格式化的 hook,没有。那道我专门写过一段、说「它再自信也得先问过我」的防手贱闸门,permissions.deny 里躺着零条规则。

我现在真正配着的 hook 只有两个,一个 SessionEnd,一个 Stop。点开一看,都是往 token 用量统计工具发通知的脚本。也就是说,我唯一还活着的两个 hook,干的是记账,跟纪律一点关系没有。

说句公道话,我不能断言自己从来没配过。可能是换过机器,可能是某次清理配置顺手删的,也可能配在别的项目里。我能确认的只有一件事:今天我打开的这两个文件里,那几道闸是空的。

更让我坐不住的是上周那篇。当时我复查自己给过的收权建议,其中一条讲人工卡点,我给的补充是:卡点这东西,前提是有人守在那儿,卡点没人守,跟没有一样

现在轮到我自己身上,情况还更省事。谈守不守都太早了,那道闸压根就没装。

我那句话说漏了半截

坐下来想了想,我发现自己那句话有个盲区。

「卡点没人守等于没有」,这句话默认了一个前提:闸已经装上了,只是没人盯。可我这次的情况还要往前一步,闸根本就没建起来

这两件事的成因完全不同。没人守是精力问题,装不上人手。而闸没装,纯粹是因为它不装也不报错。

这就是配置类东西最阴的地方。你少写一条 deny 规则,今天不会有任何提示,明天也不会,一直到某个 agent 真的执行了一条你不想让它跑的命令,你才知道那儿本来该有道闸。我之前踩过类似的坑:一个部署用的令牌悄悄过期,构建全绿,只有最后一步推不上去,网站红了整整一天我才发现。都是同一个病,缺席的东西不会举手

回头看我写那篇 hook 文章时给的判据,其实是对的:同一条规矩它反复不听,就别指望它自觉,钉成 hook。那时候我是被坑够了才划出这条线。

坑归坑,配置该掉还是会掉。

标准件解决不了这一步

所以 Google 这次做的事,到底帮我解决了什么?

有两样是真解决了。一是可移植,纪律不再是各家工具各写一套语法,配置文件挪一挪就能带走。二是拒绝理由回喂模型,省掉我手工转达那一下。

但有一样它替不了,而且这一样恰恰是我今天栽的地方:装不装、拦哪几条,还是得有个人坐下来把规则写出来

Google 给的是一副更好用的闸门零件。至于门口该不该站人,它管不着。我那三条规则丢了,怪不到工具头上,是我自己写完文章之后就没再管。

说到底,真正拦住 agent 的,不在工具给不给你这个钩子,在有没有人坐下来把那条规则写出来。

这话不是给 Google 泼冷水。工具做到标准件这一层已经很值钱了,剩下那一步本来就不该指望它。我只是提醒我自己:这次别又写完文章就完了。

今天就把闸补回去

所以这篇的可执行动作,我先给自己。

我准备做的是把那道防手贱的规则补回 permissions.deny,起手就拦两类:带通配符的删除,和强制推送。这两样错一次很难收回,正好符合我自己那条判据。

给你的动作更简单,一分钟的事:打开你的配置文件,看一眼 deny 那一栏是不是空的。

不用改任何东西,先看一眼就行。你可能跟我一样,以为自己早就配好了。

判断该拦哪几条也有现成的尺子,用我那条老判据就够:同一条规矩它跳过三回,就别再指望自觉,钉死它。一次是偶然,三次是习惯。

最后想问问你:你给 agent 设过硬拦截吗?如果设过,是一直活着,还是也跟我一样,写的时候挺认真,回头一看早就不知道去哪了?评论区聊聊,我挺想知道是不是只有我这样。