首页
/ ECC 代码评审解读:continuous-learning-v2 观察 Hook 的五层自动化会话防护如何避免自循环观察

ECC 代码评审解读:continuous-learning-v2 观察 Hook 的五层自动化会话防护如何避免自循环观察

2026-09-07 17:00:31作者:平淮齐Percy

导读

本文围绕 ECC 仓库中一份真实的历史代码评审记录展开,深入剖析一次针对 continuous-learning-v2 技能观察管道的改动 —— 通过 PreToolUse/PostToolUse Hook 收集工具调用事件,再由后台 observer 进程分析生成 "instinct" 学习成果。评审针对的核心命题是:当观察系统自身启动的自动化 Claude 会话也被当作观察对象时,会产生自我观察(self-loop),导致系统无限套娃。你将看到五层防护在 observe.sh 中如何逐层拦截自动化会话、评审指出的 CLAUDE_CODE_ENTRYPOINT 前向兼容漏洞与修复建议、以及仓库当前源码与回归测试对该建议的落地验证。


评审背景:为什么要给观察 Hook 加"防自循环"护栏

ECC 项目是一个面向 Claude Code、Codex、Opencode、Cursor 等 AI 编码工具的 agent 性能优化系统("The agent harness performance optimization system"),其中 continuous-learning-v2 是其内置的"本能学习"(instinct-based learning)技能:通过 Hook 100% 确定性捕获每一次工具调用,沉淀为 observations.jsonl,后台 observer 定期分析这些日志并产出带有置信度评分的原子化行为建议(instinct)。

这套机制的天然风险点在于:observer 自身也是一个由 Claude 驱动的自动化会话。查阅 observer-loop.sh 可以看到,后台分析进程会调用:

ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model "${ECC_OBSERVER_MODEL:-haiku}" ...

也就是 observer 每启动一次 Haiku 分析会话,就等价于触发一轮工具调用事件;如果 observe.sh 照单全收,就会陷入"观察器观察自己的观察器"的递归循环。与此同时,Hook 还必须在项目检测(detect-project.sh之前完成对自动化会话的拦截——因为项目检测会创建项目级目录并更新 projects.json,一次自动化会话就足以产生副作用(见 observe.sh 中的代码注释)。

评审文档 docs/PR-399-REVIEW-2026-03-12.md 评审的正是为解决该问题而提交的 PR #399

fix(observe): 5-layer automated session guard to prevent self-loop observations

评审针对的变更仅涉及两个文件:skills/continuous-learning-v2/hooks/observe.shskills/continuous-learning-v2/agents/observer-loop.sh(评审时 PR head 提交为 e7df0e588ceecfcd1072ef616034ccd33bb0f251)。


五层自动化会话防护:observe.sh 中的逐层拦截

要理解评审结论,先要看清这套五层防护的落地形态。当前仓库中的 observe.sh 在 Hook 读取 stdin、完成配置解析之后,用五个连续守卫把"非人类会话"挡在项目检测之前,每层都是命中即 exit 0(安静跳过,不产生副作用):

Layer 1 —— 入口类型(entrypoint)过滤

# Layer 1: entrypoint. Only interactive terminal sessions should continue.
# sdk-ts: Agent SDK sessions can be human-interactive (e.g. via Happy).
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
  cli|sdk-ts|claude-desktop|claude-vscode) ;;
  *) exit 0 ;;
esac

CLAUDE_CODE_ENTRYPOINT 是 Claude Code 暴露给 Hook 环境变量的会话入口标记,本层只放行"可能是人类交互式"的入口。注意 ${...:-cli} 的默认值写法:变量未设置时回退为 cli,确保手动注册 Hook 的传统终端场景不被误杀。

Layer 2 —— 最小化 Hook 配置(minimal profile)

[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0

自动化场景通过 ECC_HOOK_PROFILE=minimal 声明"只保留必要 Hook",观察类非核心 Hook 直接让路。

Layer 3 —— 协作式跳过开关(cooperative skip env)

[ "${ECC_SKIP_OBSERVE:-0}" = "1" ] && exit 0

这是留给调用方(如 observer-loop.sh)的显式"免观察"开关——上面提到的 observer 分析进程正是靠它自证自动化身份。

Layer 4 —— Subagent 会话拦截

_ECC_AGENT_ID=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('agent_id',''))" ...)
[ -n "$_ECC_AGENT_ID" ] && exit 0

从 Hook stdin 的 JSON 载荷里提取 agent_id,任何带 subagent 标识的会话在语义上就是自动化的,直接放行无意义。

Layer 5 —— 观察会话路径排除(path exclusions)

_ECC_SKIP_PATHS="${ECC_OBSERVE_SKIP_PATHS:-observer-sessions,.claude-mem}"

对当前工作目录做子串匹配,命中 observer-sessions.claude-mem 等已知的观察器工作目录即跳过,并支持通过 ECC_OBSERVE_SKIP_PATHS 环境变量追加自定义模式(逗号分隔,会先做首尾空白裁剪)。

这套防护的工程价值在于分层冗余:每一层都覆盖一类自动化来源(未来未知入口、显式声明、subagent、路径特征),单层失效时其余层仍能兜底。


核心发现(Medium 级):entrypoint 守卫的前向兼容漏洞

评审的核心异议集中在 Layer 1。PR #399 的实现用"已知非 cli 值集合"来界定自动化会话(评审原文列举了 sdk-tssdk-pysdk-climcpremote 等值),评审认为这留下了一个前向兼容漏洞

任何将来新出现的非 cli entrypoint 值都会落进默认分支,被当作交互式会话放行——这恰好重新引入了 PR 想要杜绝的自动化会话观察问题。

换句话说,枚举"哪些是自动化"的黑名单思路是有保质期的:每次 Claude Code 新增一种非交互入口(例如新的 SDK 绑定、新的集成渠道),这份名单就会过期一次,防护随之悄悄"老化"(age out),而且没有任何告警。

评审给出的更安全规则是把白名单逻辑反转过来——默认放行路径收窄为仅 cli,其余一切显式 entrypoint 一律视为自动化;当变量未设置时才以 cli 兜底:

case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
  cli) ;;
  *) exit 0 ;;
esac

这段建议的精髓在于三件事:

  1. 默认安全(fail-closed):未知即拒绝,而不是未知即放行;
  2. 默认值兼容(:-cli:保留未设置环境变量时的交互式行为,不影响传统安装;
  3. 删除维护负担:不再需要跟踪上游会新增哪些入口值。

合并建议与仓库后续演进的验证

评审对 PR 方向的评价是正面的,结论为 "合并前需做一处后续修改"(Needs one follow-up change before merge)

  • ✅ 它封堵了 observer-loop.sh 中 ECC 自身的自观察回路;
  • ✅ 它在 observe.sh 的正确位置(项目检测之前)叠加了多层守卫;
  • ✅ 它已经处理了"更廉价检查优先排序(cheaper-first ordering)"与"跳过路径裁剪(skip-path trimming)"这两个次生问题。

需要补的正是 entrypoint 守卫的泛化。对照当前仓库 observe.sh 的现状可以验证这一方向已经落地——守卫形态与评审建议同构:允许少量交互式入口继续,其余一律 exit 0。只是放行集从纯粹的 cli 扩为 cli|sdk-ts|claude-desktop|claude-vscode,代码注释解释了原因:Agent SDK(sdk-ts)会话可能是人类通过 Happy 之类界面交互触发的,不应一刀切;而真正非交互的 SDK 自动化仍会被 Layer 2~5 拦截。这说明在实际演进中,项目选择了"放行交互能力明确的入口 + 依赖多级兜底"的平衡方案,但"默认拒绝未来未知入口"的核心原则与评审建议完全一致。

这一推断与仓库测试相互印证:tests/hooks/observe-entrypoint-allowlist.test.js 中,允许集合被钉死为 clisdk-tsclaude-desktopclaude-vscode,拒绝集合则包含 unknown-hostclaude-codymcp ——其中 unknown-host 的存在恰恰证明"未来未知值会被拦截"是刻意设计的回归约束。


残留风险:回归测试缺位及其后续补强

评审在 Residual Risk 中留下一条明确的交付要求:

新的 shell 守卫行为仍没有专门的回归测试覆盖,最终合并应至少包含一次针对 entrypoint 与 skip-path 场景的可执行验证。

这是 shell 防护逻辑最容易翻车的地方:一旦后续重构把守卫挪到项目检测之后、或改坏 case 模式,防护会静默失效且不报错。评审因此要求"至少一次可执行验证通过"。

从仓库当前测试布局看,这一风险已被系统性补齐。测试策略分两层:

一是细粒度白名单钉死测试。 observe-entrypoint-allowlist.test.js 通过 bash -x 以指定 entrypoint 值真实拉起 observe.sh,并强制 ECC_HOOK_PROFILE=minimal 让 Layer 2 短路,再以 bash 跟踪输出中是否出现 '[' minimal = minimal ']' 标记来判别"是否通过 Layer 1"——从而精确断言某个 entrypoint 属于放行还是拦截:

node tests/hooks/observe-entrypoint-allowlist.test.js

该测试文件头注释表明它钉死的是 #2102 时期引入 claude-vscode 的回归行为(VS Code 扩展会话曾因白名单缺失而被静默丢弃,导致 VS Code 用户零观察记录)。

二是"副作用零产生"断言。 tests/hooks/hooks.test.js 中一组名为 "skips … before project detection side effects" 的用例,逐一验证 mcp entrypoint、minimal profile、ECC_SKIP_OBSERVE=1、subagent 载荷、observer-sessions 路径命中五种场景都能在项目检测副作用产生之前被拦截——这正是评审强调的"守卫必须位于 detect-project.sh 之前"的可执行回归证据。

如果要在本地手工复核这条防护,可以这样构造最小验证(模拟自动化 entrypoint 会走 Layer 1 提前退出,且不产生任何项目元数据):

echo '{"tool_name":"Read","session_id":"manual-check"}' \
  | CLAUDE_CODE_ENTRYPOINT=mcp \
    ECC_HOOK_PROFILE=minimal \
    bash skills/continuous-learning-v2/hooks/observe.sh post
echo "exit=$?  # 预期为 0,且无 observations 写入"

评审方法论沉淀:如何审一份 Bash Hook 防护改动

把这次评审提炼为可复用的方法论,有四点对同类"行为守卫类改动"极具参考价值:

评审维度 本次实践 仓库依据
默认值取向 守卫必须 fail-closed:未知 entrypoint 应按自动化处理,而非按交互处理 observe.sh*) exit 0
前置副作用检查 拦截逻辑必须跑在任何会产生磁盘/元数据副作用的逻辑之前 observe.sh 注释
成本排序 廉价检查(字符串比较)优先于昂贵检查(进程探测、文件扫描) 评审正文"cheaper-first ordering"已确认达成
回归可执行性 守卫行为必须配套可执行的脚本化断言,否则重构会静默破坏防护 tests/hooks/observe-entrypoint-allowlist.test.jshooks.test.js

同时,理解这套防护还需要认识其运行环境假设:该 observer 属于 continuous-learning-v2 技能,观察数据默认落在项目级目录;后台 observer 仅建议在 WSL2/Linux/macOS 运行(原生 Windows Git Bash 下进程会随 Hook 退出被 Job Object 回收,见 SKILL.md 的 "Observer platform support" 说明)。observer.enabled 默认关闭,需在 config.json 中显式打开:

{
  "version": "2.1",
  "observer": {
    "enabled": true,
    "run_interval_minutes": 5,
    "min_observations_to_analyze": 20
  }
}

小结:一次"小而关键"的防自循环评审

PR #399 的改动面只有两个 shell 文件,但它解决的是自学习系统最危险的失效模式——系统把自己的自动化行为当作学习素材,形成正反馈回路。评审的价值不在于推翻方向,而在于指出黑名单式守卫的"保质期问题",并用一行反转逻辑把防护变成默认安全的前向兼容设计:只允许 cli 交互入口,未知即自动化,未设置则以 cli 兜底

仓库现状表明,该建议不仅被采纳并泛化(放行交互能力明确的 SDK/桌面/编辑器入口、其余一律拦截),还配上了可执行的回归测试。对任何构建自观察、自学习管道的开发者来说,这份评审记录都是一份高质量的参考样例:如何用最少的文件变更、最多的分层冗余,以及一条清晰的 fail-closed 原则,杜绝"自我观察循环"这类隐蔽的失控。

延伸阅读(仓库内)

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389