一个看似普通的 Hook 报错
这次遇到的问题,发生在一个并不特殊的动作里:在一个专门存放工具和技能的工作目录中启动自动化会话,然后执行一条普通命令。命令本身没有什么特别,真正先出问题的是会话结束前后自动运行的 Hook。它提示找不到脚本,进一步看才发现,脚本路径被拼成了“当前目录下再套一层工具目录”的重复路径。
我一开始以为只是某个目录搬动后留下的漏改。配置里使用了项目目录变量,再接上一个相对路径。这个写法放在普通项目里看起来很自然,因为变量通常就指向项目根目录;但当会话从一个特殊的工具目录启动时,变量指向的已经不是脚本所在的上层目录。相对路径仍然按规则拼接,只是拼出了错误的位置。
很多自动化故障不是脚本不能运行,而是脚本还没有被正确找到。
这件事的麻烦在于,失败发生在主任务之外。用户可能只是运行一条命令,却先看到 Hook 报错;如果把它当成主命令失败,就会沿着错误方向排查。更糟的是,脚本若承担记录、清理或状态更新,错误可能不会立刻阻断任务,却会让后续状态不完整。
表面是路径错,实际是上下文没分清
把问题简化成“把相对路径改成绝对路径”,确实可能让眼前的报错消失,但这还不够。因为同一类配置同时影响了另一个停止阶段脚本。它们都依赖当前项目目录变量,却都默认变量一定指向包含公共工具的目录。
这说明真正需要修复的不是某一个字符串,而是路径解析的规则。全局配置会被许多项目和工作目录复用,不能假定启动位置固定;项目级配置则不同,它本来就服务于单个项目,项目目录变量通常始终指向自身。两者看起来都使用相同变量,运行边界却不一样。
如果不区分这两种上下文,最容易出现的结果是:为了修全局配置,把所有项目配置也一起改掉;或者只修当前报错的项目,下一次换到另一个特殊目录时,问题再次出现。前一种做法扩大了改动范围,后一种做法只是在追着症状走。
路径是否安全,不取决于它看起来像不像绝对路径,而取决于它是否匹配实际运行上下文。
我没有直接把路径全部写死
错误捷径很明显:把公共脚本的完整位置直接写进配置。这样做在当前机器上可能最省事,也能快速验证命令不再报错。但我没有把它当成唯一方案,因为项目内同名脚本的优先级可能本来就是设计的一部分。
我选择保留两层解析,是因为全局 Hook 既可能服务于当前项目中的定制脚本,也可能需要调用一份稳定的公共脚本。第一步先检查项目目录下的目标,能够保留原来的项目级行为;只有项目内不存在时,才回退到稳定的公共位置。这样处理,路径的优先级被写成了明确规则,而不是藏在某次启动位置里。
这里还有一个边界:两处都不存在时,不应该让一个辅助 Hook 把整个主任务拖垮。于是最后增加了安全跳过。它不是把所有错误都吞掉,而是只对“目标脚本缺失”这一类情况不动作;脚本本身若有意返回某种拦截信号,仍然交给原有流程处理。
我判断这条顺序更稳,是因为它同时照顾了兼容性和可恢复性。项目内有目标时不改变项目行为,公共位置仍可作为跨项目回退,连回退目标也不存在时不会制造新的噪音。对辅助自动化来说,找不到可选目标时幂等跳过,通常比用错误路径反复失败更安全。
一个变量,不能代表所有目录
这次修复也让我重新看待“项目目录”这个词。它不是固定地点,而是一次运行里的上下文信息。对项目级配置来说,它往往就是正确锚点;对全局配置来说,它只是调用者当前所在的位置,未必包含全局脚本。
因此,跨项目 Hook 不应该把项目目录变量当成全局根目录使用。它可以参与解析,但不能独自决定解析结果。更可靠的写法,是把目标分成两个层次:先问“当前项目有没有自己的版本”,再问“公共位置是否有稳定版本”。这个判断顺序本身就是配置的一部分,应该能被后来接手的人一眼读懂。
排查其他项目配置时,我没有看到一个报错就把所有配置全部替换,而是先检查它们的作用范围、变量用途和目标文件是否真实存在。约有25个配置文件被逐一纳入检查,最后确认同类问题只复现在全局配置中。另有几组项目级配置虽然也使用了项目目录变量,但目标都在各自项目内,因此不需要额外修改。
配置排查的终点不是“所有文件都长得一样”,而是确认每个文件的运行边界都自洽。
真实工作目录才是回归测试
修完以后,不能只在熟悉的项目根目录里运行一次命令。那样测到的只是最顺的路径:变量指向项目,项目目录下可能还有恰好存在的目标,错误的回退逻辑根本没有机会暴露。
这次验证专门模拟了那个容易出问题的特殊工作目录,再执行一条新的命令。结果是退出码为0,没有出现 Hook 报错。这个结果验证了两件事:第一,新的解析顺序在非标准目录下能够找到稳定回退;第二,辅助流程没有因为路径不匹配阻断主命令。
我还把“没有报错”和“命令确实完成”分开看。早先一次无界面验证因为本机缺少某个外部命令,并没有真正跑起来,所以不能把它当作有效证据。后面的直接执行虽然不如完整会话复杂,却准确覆盖了这次改动最需要证明的路径解析行为。
如果不做这种回归,配置很可能在日常项目里显得完全正常,直到某人从工具目录、临时目录或另一个工作树启动会话才重新出错。一个路径修复是否可靠,要看它能否在最容易触发错误的工作目录里安静通过,而不是只看常规目录里的绿色结果。
把路径解析写成可复用的方法
这次经验可以整理成一条很短的流程。先确认配置属于全局还是项目级,再确认当前变量代表的是哪个目录;然后把目标解析写成“项目内优先、稳定位置回退、目标缺失安全跳过”。如果有多个 Hook 使用同一类路径,不要只修第一个报错点,要检查它们是否共享同一个错误假设。
接着做两种检查。静态检查用于确认其他配置没有复制同样的危险写法,尤其要区分项目级文件和全局文件;动态检查则要进入至少一个非标准工作目录,执行真实的新命令,观察退出码和 Hook 输出。两者缺一不可:只看文本,可能漏掉环境组合;只跑一次,可能不知道其他配置是否仍有隐患。
这套方法也适用于构建脚本、提交检查、清理任务和本地开发工具。凡是“从任何项目都可能被调用,却要访问固定公共资源”的自动化,都应该明确自己的根目录策略。最容易误判的地方,是把“我现在所在的目录”当成“脚本应该从哪里找”。前者属于调用者,后者属于资源的归属。
表面上我是在修一个 Hook,实际上是在给自动化补上上下文边界。稳定的脚本不是永远假设环境正确,而是在环境不符合预期时仍然知道下一步该怎么做。
下次我会怎么做
下次遇到类似报错,我会先记录触发它的真实工作目录,再沿着配置找到路径变量和目标脚本,不急着把路径改成一条写死的绝对位置。第一步确认这是全局规则还是项目规则,第二步检查是否还有同类调用,第三步把解析顺序写清楚。
改动之后,我会优先测试最不标准的目录,而不是只在熟悉的项目根目录里验证。测试结果至少要回答3个问题:项目内目标是否仍然优先,公共回退是否能找到,两个目标都缺失时是否安全退出。若主命令有自己的退出语义,也要确认辅助 Hook 没有意外吞掉它。
我更愿意把这看成一种工作习惯,而不是一个路径技巧。目录会变,项目会换,自动化配置也会被不同入口复用;真正应该留下来的,是对运行上下文的判断。一个流程成熟的标志,不是它只能在作者的当前目录里跑通,而是换一个工作目录,下一位使用者仍然不需要猜它为什么找得到、找不到,以及找不到以后会发生什么。