跳到主要内容

编写插件 Hook

当插件需要在会话事件发生时计算并提供指令,可使用生命周期 Hook。如果只需要复用固定的操作步骤,优先编写 Skill。

本指南创建一个只有两个文件的插件。它的命令只输出一条指令,不读取文件、不写入状态、不访问网络,也不需要额外运行环境。

创建插件​

创建名为 hook-example 的文件夹,包含:

hook-example/
├── plugin.json
└── hooks.json
plugin.json
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "hook-example",
"version": "1.0.0",
"description": "A minimal lifecycle Hook example.",
"author": { "name": "Example Team" },
"extensions": {
"ai.trapezohe.ghast": {
"hooks": "hooks.json"
}
}
}
hooks.json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "echo GHAST_HOOK_EXAMPLE: For release reviews, distinguish checks actually run from checks only recommended.",
"timeout": 5
}
]
}
]
}
}

声明的 Hook 文件必须位于插件文件夹内,并使用相对路径。Hook 本身就能构成一个插件;本例不需要空的 Skills 目录或 JavaScript 激活文件。

启用期间,凡是经过这些 Hooks 处理的新提示词都会获得这条指令,并非只有提及 @hook-example 的会话。实际插件应在命令中适当限定规则的应用范围。

测试 Hook​

  1. 安装前,在终端运行示例命令:
    echo GHAST_HOOK_EXAMPLE: For release reviews, distinguish checks actually run from checks only recommended.
    它应该原样输出这一行并成功结束。
  2. 在 Ghast 打开 Chat 或 Coding → Plugins → Install local(本地安装)。
  3. 选择 hook-example 文件夹,而不是单个 JSON 文件。本地安装会启用插件,没有单独的能力确认步骤,因此应提前检查文件。
  4. 在 **Installed(已安装)**中找到 hook-example,确认 Status → Enabled(状态 → 已启用)。
  5. 使用 Ghast 自己的模型聊天新建会话,发送:“给我一份发布审查清单。不要调用工具或修改文件。”
  6. 确认回复完成且没有 Hook 错误。示例指导模型区分已经执行的检查与建议执行的检查;它本身不会运行检查。
  7. 实验结束后,从插件列表移除 hook-example,并新建会话进行对照。

终端测试证明的是命令输出,不代表 Ghast 集成已经成功。看起来符合预期的模型回复也不能证明 Hook 确实运行。开发脚本时,可暂时向 stderr 输出不含敏感信息的诊断标记,并在 Ghast 开发环境的启动终端中查看输出。不要记录完整提示词、环境变量或凭据。

应在用户实际使用的 Ghast 界面中测试。不要假定第三方 Coding Agent 或所有 Team 工作流都使用与 Ghast 自身聊天相同的 Hooks。

选择事件​

事件触发时机额外 JSON 输入matcher 匹配内容
SessionStart当前已加载插件处理的每个会话的首次生成请求,以及请求前自动压缩之后source:startup、resume 或 compactsource
UserPromptSubmit每个新的提示词标识执行一次prompt空字符串
SubagentStartGhast 启动受支持的子代理任务时agent_id、agent_typeagent_type

SessionStart 输出在会话内复用;请求前自动压缩事件的输出会替换它。仅重新打开会话不会重新运行该事件。手动压缩目前不会刷新 SessionStart 输出。startup 表示该会话还没有已有插件数据;resume 表示已有这些数据。因此,重启 Ghast 或重新加载插件后的首次生成请求也可能是 resume。

UserPromptSubmit 输出属于当前提示词。它并非“每次模型回复”或“每次工具调用”事件:复用同一提示词标识的重试不一定重新执行。

SubagentStart 的 agent_type 是执行模式,不是代理显示名称或职业。请使用目标任务实际提供的值;默认模式为 general-purpose。即使自然语言任务要求“审查”,reviewer matcher 也不会因此自动匹配。

对于这些子代理任务,session_id 是父对话 ID,agent_id 是子任务 ID。同一父对话的多个子任务共享该插件的 PLUGIN_DATA。如果需要保存子任务专用状态,应按 agent_id 分开保存,避免并发子任务覆盖彼此的数据。

SubagentStart 输入示例:

{
"session_id": "parent-conversation",
"hook_event_name": "SubagentStart",
"agent_id": "child-task",
"agent_type": "general-purpose"
}

这个任务启动事件不提供 cwd,因此命令在插件的会话数据文件夹运行,而不是项目目录。

添加匹配规则​

规则可以包含 JavaScript 正则表达式 matcher。省略它表示该事件每次发生时都运行该规则。例如:

{
"hooks": {
"SessionStart": [
{
"matcher": "^(startup|resume|compact)$",
"hooks": [
{
"type": "command",
"command": "echo Report verified results separately from assumptions.",
"timeout": 5
}
]
}
]
}
}

对于 UserPromptSubmit,应在脚本内检查 prompt;matcher 不会收到消息正文。例如,切换模式的脚本可以只处理某个特定用户命令,对其他请求不输出内容。

规则和命令按列表顺序执行。每个配置的事件至少需要一条规则,每条规则至少需要一条命令。无效事件名、正则表达式或配置会导致注册失败。

读取输入和定位文件​

Ghast 向命令标准输入写入一个 JSON 对象。所有事件都有 session_id 和 hook_event_name;有可用工作目录时还包含 cwd。

输入示例:

{
"session_id": "example-session",
"hook_event_name": "UserPromptSubmit",
"prompt": "Review the release checklist",
"cwd": "/work/example"
}

如果脚本需要准确读取字段,不要靠字符串片段匹配解析 JSON。使用所选运行环境的 JSON 解析器,并将运行环境列为插件的前置依赖。

环境变量值
PLUGIN_ROOT已安装的插件文件夹
CLAUDE_PLUGIN_ROOT同一插件文件夹,供兼容脚本使用
PLUGIN_DATA当前插件、当前会话的可写数据文件夹
GHAST_HOOKtrue

如果事件提供了 cwd,命令在那里运行;否则在该会话的插件数据文件夹运行。不要假定当前目录是插件根目录。

对于包内的 macOS/Linux shell 脚本,可显式通过 sh 调用,并为路径加引号:

{
"type": "command",
"command": "sh \"${PLUGIN_ROOT}/scripts/context.sh\"",
"timeout": 10
}

将 scripts/context.sh 一起打包。通过 sh 调用不要求该文件具有执行权限;直接执行脚本则需要。Windows 命令使用 cmd.exe,不是 PowerShell 或 POSIX sh。请提供 Windows 兼容命令,或明确平台要求。Ghast 会在 Windows 上将命令字符串中的三个插件路径变量转换为 Windows 环境变量语法。

PLUGIN_DATA 适合保存会话局部状态,不适合长期秘密。数据文件夹被复用,并不代表插件移除再安装后还加载着旧会话指令。

返回指令或阻止请求​

命令成功结束时,空 stdout 不增加上下文。不以 { 开头的非空 stdout 会作为文本指令处理。

结构化输出应只写入一个 JSON 对象:

{"additionalContext":"List completed checks separately from checks the user still needs to run."}

也可使用针对事件的结果:

{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Confirm the requested release environment before recommending commands."
}
}

如果提供 hookEventName,必须与当前事件一致。附加上下文必须是字符串。不要在 stdout 的 JSON 前后混入进度消息;诊断信息使用 stderr。以 { 开头的输出必须是有效 JSON。

如需停止当前生命周期流程,返回:

{"continue":false,"stopReason":"Select the target release environment before continuing."}

也可以使用 {"decision":"block","reason":"..."}。如果希望 Ghast 在错误中包含你提供的原因,应返回此类 JSON 并以退出码 0结束。退出码 2 也会阻止流程,但只产生通用退出码错误,不会从 stdout 中解析原因。这是生命周期检查,不是逐个 Tool 的审批规则。

结果行为
退出码 0,stdout 为空继续,不增加上下文
退出码 0,文本或有效上下文 JSON添加返回的上下文
退出码 0,返回阻止结果停止当前生命周期流程
退出码 2,且未超时以通用退出码错误停止;不解析 stdout
其他非零退出码,或超时记录警告,继续,但不添加该命令的上下文
退出码 0,且输出为以 { 开头的无效 JSON、事件名不一致、上下文不是文本或输出过长生命周期流程失败

先处理超时和非零退出,再处理 stdout。例如,退出码 1 即使带有无效 JSON,也只是警告并继续,不会解析该 JSON。stderr 进入诊断信息,不进入模型上下文,也不会单独阻止请求。

超时不等于拒绝。如果缺少检查结果时继续操作并不安全,不能只靠“命令会超时”来实施策略。

限制与安全​

  • **超时:**单位为秒,默认 10,必须为正数,每条命令最多 30。命令会延迟请求,应尽量快速完成。
  • **输出:**去除首尾空白后,最多 64,000 个 UTF-16 代码单元,不是字节。实际指令应远短于此,避免浪费模型上下文。
  • **权限:**脚本以操作系统账户权限运行;会话中的 Ask 模式和命令审批不负责批准这些脚本。
  • **数据共享:**stdin 可能包含消息和路径;stdout 可能发送给所选模型 Provider。相应检查网络请求和日志。
  • **范围:**是否注册取决于插件启用状态。移除会停止 Hooks,但不会撤销文件写入、远程调用或已有会话内容。

发布前验证​

测试新会话、第二条提示词,以及插件真正使用的生命周期事件。覆盖空输出、格式错误、非零退出、超时和主动阻止。通过客户端验证安装与移除,不要只检查命令行输出。

检查所支持的平台、运行环境依赖、带空格的路径,以及诊断信息中是否存在秘密。更新后重新检查脚本。按构建插件打包发布。