Headroom agent hooks 插件解析:Claude Code 与 Copilot CLI 的会话级 Hook 如何自动拉起 Headroom 运行时
Headroom agent hooks 插件解析Claude Code 与 Copilot CLI 的会话级 Hook 如何自动拉起 Headroom 运行时【免费下载链接】headroomCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.项目地址: https://gitcode.com/GitHub_Trending/head/headroomHeadroom 的headroom-agent-hooks插件为 Claude Code 和 GitHub Copilot CLI 提供轻量的启动钩子每次会话开始或执行命令前代理会调用headroom init hook ensure这个隐藏命令负责检查是否存在匹配的持久化headroom init部署durable deployment若运行时未就绪则自动拉起。读完本文你将理解该插件的 hooks 配置结构、init hook ensure的完整执行链路profile 解析、manifest 加载、运行时就绪探测与三种部署形态的启动策略以及它绝不干扰会话的防御性设计。一、插件定位会话内的自愈启动器这个插件本身不包含任何压缩逻辑它的职责非常单一确保已初始化的 Headroom 运行时在代理会话期间保持可用。插件位于 plugins/headroom-agent-hooks其 READMEplugins/headroom-agent-hooks/README.md概括了全部行为This plugin exposes lightweight startup hooks for Claude Code and GitHub Copilot CLI. The hooks callheadroom init hook ensure. That hidden helper checks for a matching durableheadroom initdeployment and starts it if needed.也就是说工作流分两步用户在代理工作目录中执行过一次headroom init针对 Claude Code、Copilot CLI 等目标安装持久化部署Headroom 会在本地磁盘写入一个部署 manifest描述该 profile 对应的运行时Docker 容器、supervisor 服务或 detached agent之后每当代理会话启动时agent hooks 插件注册的 hook 命令被执行headroom init hook ensure依据 manifest 探测运行时状态未运行则自动启动从而保证代理发出的请求始终经过 Headroom 的压缩管线tool 输出、日志、文件与 RAG 片段的 token 压缩。插件的 hooks 定义在 hooks/hooks.json全文如下{ description: Headroom plugin hooks — ensure the local Headroom runtime is available for initialized agents., hooks: { SessionStart: [ { matcher: startup|resume, hooks: [ { type: command, command: headroom init hook ensure, timeout: 15 } ] } ], PreToolUse: [ { matcher: Bash|PowerShell, hooks: [ { type: command, command: headroom init hook ensure, timeout: 15 } ] } ] } }两个事件各有分工从配置结构可以读出设计意图SessionStartmatcher 为startup|resume会话启动或恢复时执行一次 ensure覆盖打开新会话时运行时恰好没起这一最常见场景。PreToolUsematcher 为Bash|PowerShell每次执行 shell 工具前再兜底一次。从源码结构看这层冗余应对的是会话进行到一半运行时进程退出或系统休眠唤醒后未恢复的情况——只要代理还要跑命令就会触发一次轻量检查。两处 hook 都设置timeout: 15秒。注意 ensure 的内部实现下文详述在运行时已就绪时只做一次快速就绪探测即返回因此 15 秒的上限几乎永远不会被触到只有在真正需要冷启动运行时并等待其 ready内部最长等待 45 秒时才会贴近甚至超过该值——这属于可接受的边缘情况因为 hook 是 best-effort 的超时只会跳过本次确保不会中断会话。二、headroom init hook ensure的实现链路命令入口在 headroom/cli/init.py 中是一个刻意隐藏的内部命令组init.group(hook, hiddenTrue) def init_hook() - None: Internal hook helpers. init_hook.command(ensure) click.option(--profile, defaultNone, helpExplicit deployment profile to ensure.) click.option(--marker, defaultNone, hiddenTrue) def init_hook_ensure(profile: str | None, marker: str | None) - None: Best-effort ensure used by installed agent hooks.两个细节值得注意hiddenTruehook子命令组不出现在headroom init --help的帮助列表中。它是给已安装 hook 调用的内部接口不是面向用户的主命令——插件 README 中 That hidden helper 的说法正源于此。--marker选项隐藏且实现中直接del marker它存在的意义不是给 ensure 用的而是配合下文提到的_hook_command()在写入各代理 hook 配置时做去重标记识别这条 hook 是 Headroom 装的避免重复注入或误删用户自己的 hook。2.1 profile 解析本地优先全局兜底当 hook 不带--profile参数调用时插件的 hooks.json 正是如此命令按如下顺序确定要确保的部署 profileheadroom/cli/init.pyprofiles: list[str] [] if profile: profiles.append(profile) else: local_profile _local_profile() if _has_manifest(local_profile): profiles.append(local_profile) elif _has_manifest(_GLOBAL_PROFILE): profiles.append(_GLOBAL_PROFILE) for name in profiles: _ensure_profile_running(name)显式传--profile时只处理该 profile否则先探测本地 profile_local_profile()基于当前工作目录推导对应在这个项目目录里headroom init过的部署本地没有 manifest 时退回全局 profile常量_GLOBAL_PROFILE init-user对应headroom init -g这类全局安装。_has_manifest()的写法体现了 hook 场景的第一原则——不能崩def _has_manifest(name: str) - bool: # Best-effort: a corrupt manifest must not crash the session-start hook. try: return load_manifest(name) is not None except ManifestError: return False2.2 manifest 加载与损坏容忍manifest 的读写实现在 headroom/install/state.py。load_manifest()加载指定 profile 的manifest.json语义是文件不存在 → 返回None表示该 profile 从未执行过headroom inithook 安静退出什么都不做文件存在但解析失败JSON 损坏、schema 漂移、手工编辑破坏结构→ 抛出类型化异常ManifestError而不是让原始 traceback 泄漏到 hook 的 stdout/stderr——对 agent hook 而言向 stdout 输出意外内容可能污染代理上下文因此必须拦截。load_manifest()中还包含一个迁移逻辑把旧的个人仓库镜像地址ghcr.io/chopratejas/headroom重写为组织仓库ghcr.io/headroomlabs-ai/headroom保留 tag避免老 manifest 静默地跑在落后的镜像版本上。写入侧同样做了健壮性处理save_manifest()通过临时文件 fsyncos.replace原子重命名落盘即使保存过程中进程被 kill磁盘上也只会留下旧的完整文件或新的完整文件不会出现截断的 manifest。这与 hook 侧损坏 manifest 必须容忍的策略互为补充。2.3 运行时确保探测 → 加锁 → 按形态启动核心的_ensure_profile_running(profile)headroom/cli/init.pyL738 起执行如下决策链def _ensure_profile_running(profile: str) - None: # Best-effort hook path: a corrupt manifest must not crash the session. try: manifest load_manifest(profile) except ManifestError: return if manifest is None: return with _suppress_hook_output(): if wait_ready(manifest, timeout_seconds1): return try: with acquire_runtime_start_lock(manifest.profile) as acquired: if not acquired: return if wait_ready(manifest, timeout_seconds1): return if runtime_status(manifest) running: if wait_ready(manifest, timeout_seconds_STARTUP_READY_TIMEOUT_SECONDS): return stop_runtime(manifest) if manifest.preset InstallPreset.PERSISTENT_DOCKER.value: start_persistent_docker(manifest) elif manifest.supervisor_kind SupervisorKind.SERVICE.value: start_supervisor(manifest) else: start_detached_agent(manifest.profile) wait_ready(manifest, timeout_seconds45) except Exception: return逐步解读这条链路快速路径1 秒wait_ready(manifest, timeout_seconds1)——绝大多数调用发生时运行时已在运行一次探测即可返回。这是 hook 在PreToolUse上每次 shell 命令都触发却不拖慢会话的关键。启动互斥锁acquire_runtime_start_lock()保证同一 profile 在同一时刻只有一个进程负责拉起运行时。会话 A 的 SessionStart hook 和会话 B 的 PreToolUse hook 并发触发时后到者拿不到锁直接返回不会重复启动。拿锁后二次探测锁竞争期间别人可能已经完成了启动再wait_ready一次是典型的 double-check。僵死处理若runtime_status显示running但一直不 ready进程活着但端口不通先stop_runtime()再重启避免在坏进程上反复重试。按部署形态启动三选一PERSISTENT_DOCKER预设 →start_persistent_docker(manifest)拉起持久化 Docker 部署SupervisorKind.SERVICE→start_supervisor(manifest)由系统级 supervisor 管理服务进程其他 →start_detached_agent(manifest.profile)启动分离的后台代理进程。最终等待wait_ready(timeout_seconds45)给冷启动留足窗口。整条链路还包在_suppress_hook_output()上下文里L717 起它用os.dup2把 fd 1/2 临时指向/dev/null同时redirect_stdout/redirect_stderr确保启动过程中的任何日志、警告都不会泄漏进 hook 的输出流finally 块保证 fd 一定恢复。整个函数以except Exception: return收尾——任何异常都被吞掉hook 永远静默成功。三、与代理侧 hook 配置的关系插件 hooks.json 是用户装插件路径下的声明式配置而headroom init走的是主动注入路径两者最终都指向同一条命令。_hook_command()生成的命令形如headroom init hook ensure --profile profile由以下函数分别合并写入各代理的配置_ensure_claude_hooks()→ Claude Code 的 hooks 配置~/.claude作用域_ensure_copilot_hooks()→ Copilot CLI 配置命令带--marker marker_ensure_codex_hooks()→.codex/hooks.json全局或本地作用域并同步确保[features].hooks开关为 true含旧键名codex_hooks的迁移处理。这些注入函数统一采用按事件合并 基于 marker 去重的读写策略read-merge-write源码注释明确说明原因不能整体覆盖 payload否则会摧毁用户自己维护的其他 hook 与顶层键。这与第二节中--marker选项的用途闭环marker 既是Headroom 装过的指纹也是幂等注入的锚点。因此对最终用户而言有两种等效到达方式执行headroom init可按需加-g全局作用域、指定 backend/region 等参数由 Headroom 主动把带--profile的 ensure 命令注入代理配置安装headroom-agent-hooks插件由插件的 hooks.json 提供不带 profile 参数的通用 ensure 调用。前一种路径 profile 是显式绑定的后一种依赖第二节描述的本地 profile 优先、全局兜底自动解析。四、为什么这样设计会话内 hook 的三条纪律从插件与其后端实现的整体结构可以提炼出 agent hook 编程的三条纪律Headroom 全部遵守静默_suppress_hook_output()重定向 fd异常全部吞掉。agent hook 的 stdout 可能被宿主代理消费任何意外输出都可能破坏会话所以宁可什么都不输出。幂等且并发安全ready 探测 启动锁 double-check使每会话一次和每次 shell 命令一次两种高频触发方式叠加也不会产生重复启动或竞态。数据损坏降级manifest 不存在 → 静默跳过manifest 损坏 →ManifestError捕获后返回。session-start hook 的失败代价本次会话无压缩远小于崩溃代价会话中断或上下文污染代码用连续的 best-effort 注释把这一取舍写明。五、使用要点小结插件目录仅含两份文件README.md 与 hooks/hooks.json安装后它只向代理注册 SessionStart 与 PreToolUse 两个 command hook命令固定为headroom init hook ensure超时 15 秒命令是headroom init子树下的隐藏命令hiddenTrue支持--profile显式指定部署 profile不指定时按当前目录的本地 profile → 全局init-userprofile顺序寻找 manifest只有在该机器上对相应 profile 执行过headroom init磁盘存在manifest.json时ensure 才会真正去拉起运行时否则命令空转并静默退出对未初始化 Headroom 的环境零影响运行时启动策略取决于 manifest 记录的部署形态持久化 Docker、supervisor 服务或 detached agent就绪等待上限 45 秒实现可追踪的源码路径命令入口与 profile 决策在 headroom/cli/init.pymanifest 持久化与损坏处理在 headroom/install/state.py。这套插件声明 hook 隐藏命令 best-effort 自愈的组合让 Headroom 在 Claude Code / Copilot CLI 场景下做到了会话一开、运行时必在而用户既不需要常驻看门脚本也看不到任何 hook 噪音。【免费下载链接】headroomCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.项目地址: https://gitcode.com/GitHub_Trending/head/headroom创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考