- 首頁
- Documentation
- 設定
- Hooks
Hooks
本文件描述 src/extensibility/hooks/* 中的目前 hook 子系統程式碼。
執行時期的目前狀態
Section titled “執行時期的目前狀態”Hook 套件(src/extensibility/hooks/)仍作為 API 介面匯出並可使用,但預設的 CLI 執行時期現在初始化的是擴充執行器路徑。在目前的啟動流程中:
--hook被視為--extension的別名(CLI 路徑會合併至additionalExtensionPaths)- 工具由
ExtensionToolWrapper包裝,而非HookToolWrapper - 情境轉換與生命週期發射透過
ExtensionRunner進行
因此本文件記錄的是 hook 子系統本身的實作(型別/載入器/執行器/包裝器),包含舊版行為與限制。
src/extensibility/hooks/types.ts— hook 情境、事件型別與結果契約src/extensibility/hooks/loader.ts— 模組載入與 hook 探索橋接src/extensibility/hooks/runner.ts— 事件派送、命令查找與錯誤訊號src/extensibility/hooks/tool-wrapper.ts— 工具前/後攔截包裝器src/extensibility/hooks/index.ts— 匯出/重新匯出
Hook 模組是什麼
Section titled “Hook 模組是什麼”Hook 模組必須預設匯出一個工廠函式:
import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function hook(pi: HookAPI): void { pi.on("tool_call", async (event, ctx) => { if (event.toolName === "bash" && String(event.input.command ?? "").includes("rm -rf")) { return { block: true, reason: "blocked by policy" }; } });}工廠函式可以:
- 透過
pi.on(...)註冊事件處理器 - 透過
pi.sendMessage(...)傳送持久性自訂訊息 - 透過
pi.appendEntry(...)持久化非 LLM 狀態 - 透過
pi.registerCommand(...)註冊斜線命令 - 透過
pi.registerMessageRenderer(...)註冊自訂訊息渲染器 - 透過
pi.exec(...)執行 shell 命令
discoverAndLoadHooks(configuredPaths, cwd) 的執行步驟:
- 從能力登錄檔載入已探索的 hooks(
loadCapability("hooks")) - 附加明確設定的路徑(依絕對路徑去重)
- 呼叫
loadHooks(allPaths, cwd)
loadHooks 接著匯入每個路徑並預期其具有 default 函式。
loader.ts 解析 hook 路徑的方式如下:
- 絕對路徑:直接使用
~路徑:展開後使用- 相對路徑:相對於
cwd解析
重要的舊版不一致
Section titled “重要的舊版不一致”hookCapability 的探索提供者仍以前/後 shell 風格的 hook 檔案為模型(例如 .claude/hooks/pre/*、.xcsh/.../hooks/pre/*)。
此處的 hook 載入器使用動態模組匯入,並需要一個預設的 JS/TS hook 工廠函式。若探索到的 hook 路徑無法作為模組匯入,載入將失敗並回報於 LoadHooksResult.errors。
Hook 事件在 types.ts 中具有強型別定義。
Session 事件
Section titled “Session 事件”session_startsession_before_switch→ 可回傳{ cancel?: boolean }session_switchsession_before_branch→ 可回傳{ cancel?: boolean; skipConversationRestore?: boolean }session_branchsession_before_compact→ 可回傳{ cancel?: boolean; compaction?: CompactionResult }session.compacting→ 可回傳{ context?: string[]; prompt?: string; preserveData?: Record<string, unknown> }session_compactsession_before_tree→ 可回傳{ cancel?: boolean; summary?: { summary: string; details?: unknown } }session_treesession_shutdown
代理程式/情境事件
Section titled “代理程式/情境事件”context→ 可回傳{ messages?: Message[] }before_agent_start→ 可回傳{ message?: { customType; content; display; details } }agent_startagent_endturn_startturn_endauto_compaction_startauto_compaction_endauto_retry_startauto_retry_endttsr_triggeredtodo_reminder
工具事件(前/後模型)
Section titled “工具事件(前/後模型)”tool_call(執行前)→ 可回傳{ block?: boolean; reason?: string }tool_result(執行後)→ 可回傳{ content?; details?; isError? }
這是 hook 子系統的核心前/後攔截模型。
Hook 工具攔截流程
tool_call 處理器 │ ├─ 任何 { block: true }?── 是 ──> 拋出例外(工具已封鎖) │ └─ 否 │ ▼ 執行底層工具 │ ├─ 成功 ──> tool_result 處理器可覆寫 { content, details } │ └─ 錯誤 ──> 發射 tool_result(isError=true) 後重新拋出原始錯誤執行模型與變異語意
Section titled “執行模型與變異語意”1)執行前:tool_call
Section titled “1)執行前:tool_call”HookToolWrapper.execute() 在工具執行前發射 tool_call。
- 若任何處理器回傳
{ block: true },執行停止 - 若處理器拋出例外,包裝器以安全失敗(fail-closed)方式封鎖執行
- 回傳的
reason將成為拋出的錯誤訊息
2)工具執行
Section titled “2)工具執行”若未被封鎖,底層工具正常執行。
3)執行後:tool_result
Section titled “3)執行後:tool_result”成功後,包裝器發射 tool_result 並帶有:
toolName、toolCallId、inputcontentdetailsisError: false
若處理器回傳覆寫值:
content可替換結果內容details可替換結果詳情
工具失敗時,包裝器發射帶有 isError: true 及錯誤文字內容的 tool_result,然後重新拋出原始錯誤。
Hooks 可變異的內容
Section titled “Hooks 可變異的內容”- 單次呼叫的 LLM 情境,透過
context(messages替換鏈) - 成功工具呼叫的工具輸出內容/詳情(
tool_result路徑) - 代理程式啟動前注入的訊息,透過
before_agent_start - 透過
session_before_*與session.compacting取消/自訂壓縮/樹狀行為
在此實作中 Hooks 無法變異的內容
Section titled “在此實作中 Hooks 無法變異的內容”- 原地修改工具輸入參數(
tool_call上只能封鎖/允許) - 工具錯誤拋出後繼續執行(錯誤路徑會重新拋出)
- 包裝器行為中的最終成功/錯誤狀態(回傳的
isError已有型別定義,但HookToolWrapper不會套用)
排序與衝突行為
Section titled “排序與衝突行為”探索層級的排序
Section titled “探索層級的排序”能力提供者依優先順序排序(較高者優先)。去重依據能力金鑰,先者優先。
對於 hooks,能力金鑰為 ${type}:${tool}:${name}。來自較低優先順序提供者的重複項目會被標記並從有效探索清單中排除。
discoverAndLoadHooks 建立一個扁平的 allPaths 清單,依解析後的絕對路徑去重,然後 loadHooks 依序迭代。每個探索目錄中的檔案順序取決於 readdir 的輸出;hook 載入器不進行額外排序。
執行時期處理器順序
Section titled “執行時期處理器順序”在 HookRunner 內部,順序由註冊序列決定,具有確定性:
- hooks 陣列順序
- 每個 hook/事件的處理器註冊順序
依事件型別的衝突行為:
tool_call:最後回傳的結果優先,除非某個處理器封鎖;第一個封鎖立即短路tool_result:最後回傳的覆寫值優先(無短路)context:鏈式執行;每個處理器接收前一個處理器的訊息輸出before_agent_start:第一個回傳的訊息被保留;後續訊息忽略session_before_*:追蹤最後回傳的結果;cancel: true立即短路session.compacting:最後回傳的結果優先
命令/渲染器衝突:
getCommand(name)回傳跨 hooks 的第一個匹配(先載入者優先)getMessageRenderer(customType)回傳第一個匹配getRegisteredCommands()回傳所有命令(不去重)
UI 互動(HookContext.ui)
Section titled “UI 互動(HookContext.ui)”HookUIContext 包含:
select、confirm、input、editornotifysetStatuscustomsetEditorText、getEditorTextthemegetter
ctx.hasUI 表示互動式 UI 是否可用。
無 UI 執行時,預設的空操作情境行為為:
select/input/editor回傳undefinedconfirm回傳falsenotify、setStatus、setEditorText為空操作getEditorText回傳""
透過 ctx.ui.setStatus(key, text) 設定的 hook 狀態文字:
- 依金鑰儲存
- 依金鑰名稱排序
- 經過清理(
\r、\n、\t→ 空格;重複空格合併) - 合併後依寬度截斷以供顯示
錯誤傳播與回退
Section titled “錯誤傳播與回退”- 無效模組或缺少預設匯出 → 擷取至
LoadHooksResult.errors - 其他 hooks 繼續載入
HookRunner.emit(...) 針對大多數事件捕捉處理器錯誤,並向監聽器發射 HookError(hookPath、event、error),然後繼續執行。
emitToolCall(...) 較為嚴格:處理器錯誤在此不會被吞噬;它們會傳播至呼叫端。在 HookToolWrapper 中,這會封鎖工具呼叫(安全失敗)。
實際 API 範例
Section titled “實際 API 範例”封鎖不安全的 bash 命令
Section titled “封鎖不安全的 bash 命令”import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "bash") return; const cmd = String(event.input.command ?? ""); if (!cmd.includes("rm -rf")) return;
if (!ctx.hasUI) return { block: true, reason: "rm -rf blocked (no UI)" }; const ok = await ctx.ui.confirm("Dangerous command", `Allow: ${cmd}`); if (!ok) return { block: true, reason: "user denied command" }; });}執行後對工具輸出進行遮蔽
Section titled “執行後對工具輸出進行遮蔽”import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.on("tool_result", async event => { if (event.toolName !== "read" || event.isError) return;
const redacted = event.content.map(chunk => { if (chunk.type !== "text") return chunk; return { ...chunk, text: chunk.text.replaceAll(/API_KEY=\S+/g, "API_KEY=[REDACTED]") }; });
return { content: redacted }; });}每次 LLM 呼叫時修改模型情境
Section titled “每次 LLM 呼叫時修改模型情境”import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.on("context", async event => { const filtered = event.messages.filter(msg => !(msg.role === "custom" && msg.customType === "debug-only")); return { messages: filtered }; });}使用命令安全情境方法註冊斜線命令
Section titled “使用命令安全情境方法註冊斜線命令”import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.registerCommand("handoff", { description: "Create a new session with setup message", handler: async (_args, ctx) => { await ctx.waitForIdle(); await ctx.newSession({ parentSession: ctx.sessionManager.getSessionFile(), setup: async sm => { sm.appendMessage({ role: "user", content: [{ type: "text", text: "Continue from prior session summary." }], timestamp: Date.now(), }); }, }); }, });}src/extensibility/hooks/index.ts 匯出:
- 載入 API(
discoverAndLoadHooks、loadHooks) - 執行器與包裝器(
HookRunner、HookToolWrapper) - 所有 hook 型別
execCommand重新匯出
套件根目錄(src/index.ts)將 hook 型別重新匯出,作為舊版相容介面。