文档反馈
产品介绍
产品计费
企业管理
通用设置
用量管理
安全设置
个人设置
API 参考
问题排查
相关协议

企业 Hook 配置详解

本文档介绍企业 Hook 的配置方式、执行环境、输入输出规范,以及各类事件的触发与处理机制。

Hook 配置

Hook 配置格式

每个 Hook 事件配置为一个 JSON 数组。数组中的每个对象表示一个 Hook 组,matcher 用于匹配工具名称或通知类型;SessionStartUserPromptSubmitStop 三类非工具匹配事件无需配置 matcher

[
  {
    "matcher": "{ToolPattern}",
    "loop_limit": 5,
    "hooks": [
      {
        "type": "http",
        "url": "{URL to send the POST request to}",
        "timeout": 30
      }
    ]
  }
]

字段说明

Hook 相关字段的说明如下:

  • Hook 组层
    字段 类型 是否必填 描述

    matcher

    string

    匹配规则,支持正则表达式(如 Edit\|Writemcp.*)。配置为 *、空字符串或省略时,表示匹配所有工具或通知类型。

    提示matcher 字段仅对 PreToolUsePostToolUseNotification 事件有效。

    loop_limit

    number

    Stop 事件的循环次数限制。

    loop_countloop_limit 时,该 Hook 组将被跳过。

    该字段仅支持正整数;未配置或配置的值小于等于 0 时,使用默认值 5

    提示loop_limit 字段仅对 Stop 事件有效。

    hooks array 该 Hook 组下要执行的 Hook 列表。
  • Hook 定义层
    字段 类型 是否必填 描述
    type string Hook 类型,企业 Hook 当前仅支持 HTTP 类型,取值为 http
    url string 接收 Hook 请求的企业服务端 URL。TRAE 会向该 URL 发送 HTTP POST 请求。
    timeout number 请求超时时间,单位为秒,默认值为 30

HTTP 调用约定

企业 Hook 通过 HTTP 请求调用企业自有服务。TRAE 会向配置的 url 发送 POST 请求,并通过 HTTP 状态码和响应体判断后续执行行为。

  • 请求方法POST
  • 请求头Content-Type: application/json
  • 请求体:当前 Hook 事件的 JSON payload。通用字段见 “请求体通用字段” 部分,事件专有字段见 “Hook 事件” 部分。
  • 超时策略:超过 timeout 配置的时长仍未收到响应时,按执行失败处理。
  • 状态码
    HTTP 状态码 响应体 行为
    2xx 继续执行,无附加行为。
    2xx 纯文本 SessionStartUserPromptSubmit 事件中,将文本作为上下文附加给模型。若为其他事件,则继续执行。
    2xx JSON 继续执行,并按 JSON 内容执行流程控制;若返回 continue: false,则让智能体停止执行。
    2xx 任意 阻断当前问答。
    超时 / 连接失败 阻断当前问答。

    提示

    企业 Hook 依赖企业服务端的可用性。建议为 Hook 服务配置超时、监控和降级策略,避免因服务异常影响用户问答流程。

Hook 输入和输出

企业 Hook 的输入输出由 HTTP 请求体和响应体承载:请求体用于传递当前事件上下文,响应体用于返回流程控制结果或附加上下文。

请求体通用字段

每个 Hook 事件的请求体 JSON 均包含以下通用字段:

{
  "session_id": "session_id",
  "cwd": "/path/to/workspace",
  "hook_event_name": "PreToolUse",
  "workspace_roots": ["/path/to/workspace"],
  "agent_id": "agent_id",
  "agent_type": "agent_type",
  "email": "user@example.com",
  "model": "model"
}
字段 类型 描述
session_id string 当前会话 ID。
cwd string 当前会话所在的工作区目录。
hook_event_name string 当前 Hook 事件的名称。
workspace_roots string[] 如果存在多个工作区,此字段将包含所有工作区的根目录。
agent_id string 当前执行 Hook 的智能体 ID。
agent_type string 当前执行 Hook 的智能体类型。
email string 触发本次 Hook 的成员邮箱。
model string 当前会话使用的模型标识。

响应体通用字段

端点可在响应体中返回两种格式的数据:

  • JSON :用于结构化地控制智能体的执行流程。
  • 纯文本:输出内容将作为附加上下文提供给模型。此格式仅适用于 SessionStartUserPromptSubmit 事件。

对于 JSON 格式的输出,所有事件均支持以下通用流程控制字段:

{
  "continue": true,
  "stopReason": "string"
}
字段 类型 描述
continue boolean 智能体是否在 Hook 执行完毕后继续执行。默认值为 true。若设置为 false,智能体将停止执行。该字段优先于任何事件特定的 decision 字段。
stopReason string continuefalse 时,展示给用户的智能体停止执行的原因。

提示

Notification 是非阻断事件,不使用响应体进行流程控制。

Hook 事件

SessionStart

  • 触发时机:创建 Session 后、发起第一个对话之前触发。
  • Hook 的作用:在会话开始时向模型注入上下文信息。
  • 请求体
    {
      "session_id": "...",
      "hook_event_name": "SessionStart",
      "source": "startup"
    }
    
    该事件的专有字段如下:
    字段 类型 描述
    source string 会话的来源。目前仅支持 startup(新建会话)。
  • 响应体
    • 格式一:纯文本
      直接返回纯文本内容,将其作为附加上下文提供给模型。
    • 格式二:JSON
      {
        "hookSpecificOutput": {
          "hookEventName": "SessionStart",
          "additionalContext": "文本内容"
        }
      }
      
      字段 类型 描述
      additionalContext string 附加给模型的上下文。
  • 失败行为:如果 Hook 服务返回非 2xx 状态码、请求超时或连接失败,按 HTTP 调用约定处理。

UserPromptSubmit

  • 触发时机:用户发送消息后、智能体开始处理前。
  • Hook 的作用:拦截不允许的请求,或向模型附加上下文。
  • 请求体
    {
      "session_id": "...",
      "hook_event_name": "UserPromptSubmit",
      "prompt": "用户输入的 Prompt"
    }
    
    该事件的专有字段如下:
    字段 类型 描述
    prompt string 用户提交的 Prompt 文本。
  • 响应体
    • 格式一:纯文本
      直接返回非 JSON 格式的纯文本内容,将其作为附加上下文提供给模型。
    • 格式二:JSON
      {
        "decision": "block",
        "reason": "该请求不被允许的原因",
        "hookSpecificOutput": {
          "hookEventName": "UserPromptSubmit",
          "additionalContext": "附加给模型的上下文"
        }
      }
      
      字段 类型 描述
      decision string 该字段仅支持设为 block。设置后,将禁止智能体执行该 Prompt。如需允许智能体执行该 Prompt,请将该字段留空。
      reason string decision 字段的值为 block 时,此字段的内容将作为错误信息展示给用户。否则,该字段将被忽略。
      additionalContext string 附加给模型的上下文文本。
  • 失败行为:如果 Hook 服务返回非 2xx 状态码、请求超时或连接失败,按 HTTP 调用约定处理。

PreToolUse

  • 触发时机:智能体发起工具调用后、实际执行前。
  • Hook 的作用:校验或拦截工具调用、修改工具参数,或要求用户确认后再执行。
  • matcher 字段配置:可通过 matcher 字段配置正则表达式,从而匹配特定的工具名。
  • 请求体
    {
      "session_id": "...",
      "hook_event_name": "PreToolUse",
      "tool_use_id": "toolcall-id-string",
      "tool_name": "RunCommand",
      "llm_tool_name": "RunCommand",
      "tool_input": { ... }
    }
    
    该事件的专有字段如下:
    字段 类型 描述
    tool_use_id string 工具调用的唯一 ID。
    tool_name string 标准化的工具名称。详见 “PreToolUse 和 PostToolUse 事件支持的工具” 部分。
    llm_tool_name string 传递给大语言模型的原始工具名称。
    tool_input object 工具输入参数。
  • 响应体
    {
      "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "allow", 
        "permissionDecisionReason": "决策原因说明",
        "updatedInput": { ... },
        "additionalContext": "附加给模型的上下文"
      }
    }
    
    字段 类型 说明

    permissionDecision

    string

    权限决策,用于决定是否执行本次工具调用。可选值包括:

    • allow:允许执行。
    • deny:拒绝执行。
    • ask:弹出确认框,由用户决定是否执行。

    特殊情况说明

    • 如果多个 PreToolUse 事件的 Hook 正并行执行,permissionDecision 只会返回一个最终值。取值优先级为: deny -> ask -> allow
    • 如果返回值为 allow,但是该工具的运行模式为手动确认,则仍以工具运行模式为准,需要用户确认。
    permissionDecisionReason string 权限决策的原因。
    updatedInput object 修改后的工具输入参数,将整体覆盖替换原始参数(非合并更新)。
    additionalContext string 附加给模型的上下文文本。
  • 失败行为:如果 Hook 服务返回非 2xx 状态码、请求超时或连接失败,按 HTTP 调用约定处理。

PostToolUse

  • 触发时机:工具调用实际执行完成后。
  • Hook 的作用:校验执行结果或附加上下文。
  • matcher 字段配置:可通过 matcher 字段配置正则表达式,从而匹配特定的工具名。
  • 请求体
    {
      "session_id": "...",
      "hook_event_name": "PostToolUse",
      "tool_use_id": "toolcall-id-string",
      "tool_name": "RunCommand",
      "llm_tool_name": "RunCommand",
      "tool_input": { ... },
      "tool_response": { ... }
    }
    
    该事件的专有字段如下:
    字段 类型 描述
    tool_use_id string 工具调用的唯一 ID。
    tool_name string 标准化的工具名称。详见 “PreToolUse 和 PostToolUse 事件支持的工具” 部分。
    llm_tool_name string 传递给大语言模型的原始工具名称。
    tool_input object 工具输入参数。
    tool_response object 工具调用的结果。
  • 响应体
    {
      "decision": "block",
      "reason": "阻断原因",
      "hookSpecificOutput": {
        "hookEventName": "PostToolUse",
        "additionalContext": "附加给模型的上下文"
      }
    }
    
    字段 类型 描述
    decision string 该字段仅支持设为 block。设置后,会向模型传递一条阻断信息,表示工具已执行且无法撤销。如需允许智能体继续处理工具调用的结果,将该字段留空。
    reason string decision 字段的值为 block 时,此字段的内容将作为阻断原因展示给用户。否则,该字段将被忽略。
    additionalContext string 附加给模型的上下文文本。
  • 失败行为:如果 Hook 服务返回非 2xx 状态码、请求超时或连接失败,按 HTTP 调用约定处理。

Stop

  • 触发时机:智能体完成输出、准备结束当前查询时。此时,你可以检查智能体的输出是否达标;若不达标,可以阻止智能体结束任务并要求其继续处理。
  • Hook 的作用:阻止智能体结束当前任务,并要求其继续执行。
  • 请求体
    {
      "session_id": "...",
      "hook_event_name": "Stop",
      "stop_hook_active": false,
      "loop_count": 0, 
      "last_assistant_message": "大语言模型最终输出的文本内容"
    }
    
    该事件的专有字段如下:
    字段 类型 描述
    stop_hook_active boolean 当前查询是否已经被 Stop 事件的 Hook 至少阻断过一次。

    loop_count

    number

    当前查询的 Stop 事件被 Hook 阻断的次数计数。从 0 开始累加。

    循环限制:你可以通过 loop_limit 字段配置该 Hook 组允许阻断 Stop 事件的最大次数。当 loop_countloop_limit 时,该 Hook 组将被跳过,使智能体不再执行,以避免无限循环。loop_limit 的默认值为 5

    last_assistant_message string 大语言模型最终输出的文本内容。
  • 响应体
    {
      "decision": "block",
      "reason": "请继续检查测试是否通过"
    }
    
    字段 类型 描述
    decision string 该字段仅支持设为 block。设置后,将阻断智能体停止执行。如需让智能体停止执行,将该字段留空。
    reason string decision 字段的值为 block 时,此字段的内容将作为新的用户请求让智能体继续执行。否则,该字段将被忽略。
  • 失败行为:如果 Hook 服务返回非 2xx 状态码、请求超时或连接失败,按 HTTP 调用约定处理。
  • 决策控制流程:
    Stop 事件的决策控制逻辑如下:
    智能体准备停止
        │
        ▼
    检查 loop_count 是否大于等于 loop_limit?──── 是 ──► 跳过 Hook,允许智能体停止
        │
        否
        │
        ▼
    调用 Stop 事件的企业 Hook 服务
        │
        ├── 返回 2xx 状态码,且 decision 字段为空 ───────► 允许停止
        │
        ├── 返回 2xx 状态码,且 decision 字段的值为 block ──► 阻断停止,将 reason 字段作为新 Query
        │
        └── 非 2xx 状态码 / 超时 / 连接失败 ───────────► 按 HTTP 调用约定处理
    

Notification

  • 触发时机:智能体的工具调用等待用户确认时,或智能体完成任务时。该事件异步执行,不会阻塞智能体的主流程。
  • Hook 的作用:发送通知,不改变智能体的执行流程。
  • matcher 字段配置:基于通知类型(notification_type)匹配,而不是基于工具名匹配。未配置 matcher,或将其配置为空字符串或 * 时,表示匹配所有通知类型。
  • 请求体
    {
      "session_id": "...",
      "hook_event_name": "Notification",
      "notification_type": "idle_prompt",
      "message": "智能体已完成任务",
      "tool_use_id": "toolu_xxx"
    }
    
    该事件的专有字段如下:
    字段 类型 描述
    notification_type string 通知类别,用于标识通知场景,也用于匹配 matcher 字段的配置。可选值见下表。
    message string 通知的正文。

    tool_use_id

    string?

    关联的工具调用 ID。

    仅工具调用相关的通知类型携带该 ID,例如 permission_promptdocument_review 等。任务完成时发送的 idle_prompt 类通知不携带该 ID。

    notification_type 字段的值和相应触发时机如下:
    触发时机
    idle_prompt 智能体完成当前任务。
    permission_prompt 工具调用需要用户确认后才能继续执行,例如当 PreToolUse 事件的 Hook 返回 ask 决策,或工具本身需要手动确认时。
    document_review Plan 或 Spec 工作流中的文档审阅流程。
    ask_user_question 智能体需要用户补充信息时,进行提问的通知。
    browser_interaction 浏览器交互等待通知。
  • 响应体:该事件不使用响应体进行流程控制。即使服务端返回 JSON,也不会改变智能体的行为。
  • 失败行为Notification 为非阻断事件。即使 Hook 服务返回非 2xx 状态码、请求超时或连接失败,也不会改变智能体的执行流程。

PreToolUse 和 PostToolUse 事件支持的工具

PreToolUsePostToolUse 事件中,你可以通过 matcher 字段匹配 tool_name

tool_name 为标准化工具名称,取值如下:

分类 工具名称 描述
文件读取 Read 读取文件内容。
文件写入 Write 写入文件。
文件编辑 Edit 单次查找并替换文件内容。
搜索 Glob 基于文件路径模式进行匹配搜索。

Grep

基于正则表达式进行内容搜索。

LS

列出目录下的文件与子目录。

终端 RunCommand 执行终端命令。
网络 WebSearch 网络搜索。

WebFetch

获取网页内容。

交互 AskUserQuestion 向用户提问。
Skill Skill 加载 Skill。

MCP

mcp__<serverName>__<toolName>

MCP 工具。

MCP 工具匹配说明:在 Hook 中,MCP 工具的标准化名称格式为 mcp__<serverName>__<toolName>(例如 mcp__Git__iCube__git_status)。你可以在 matcher 字段中使用 mcp__.* 来匹配所有 MCP 工具,或使用具体工具的名称进行精确匹配。

示例

事件配置

[
  {
    "hooks": [
      {
        "type": "http",
        "url": "http://localhost:9000/session-start"
      }
    ]
  }
]
[
  {
    "hooks": [
      {
        "type": "http",
        "url": "http://localhost:9000/user-prompt-submit"
      }
    ]
  }
]
[
  {
    "matcher": "Read",
    "hooks": [
      {
        "type": "http",
        "url": "http://localhost:9000/pre-tool-use/block-read"
      }
    ]
  }
]
[
  {
    "hooks": [
      {
        "type": "http",
        "url": "http://localhost:9000/post-tool-use"
      }
    ]
  }
]
[
  {
    "hooks": [
      {
        "type": "http",
        "url": "http://localhost:9000/stop"
      }
    ]
  }
]
[
  {
    "hooks": [
      {
        "type": "http",
        "url": "http://localhost:9000/notification"
      }
    ]
  }
]

请求体示例

{
  "session_id": "6a321771xxxxxx1312bfebd8",
  "cwd": "/Users/example/Documents/code/hooks_test",
  "hook_event_name": "SessionStart",
  "workspace_roots": [
    "/Users/example/Documents/code/hooks_test"
  ],
  "agent_id": "dev_agent",
  "agent_type": "dev_agent",
  "source": "startup",
  "email": "zhangsan@example.com",
  "model": "auto/Trae"
}
{
  "session_id": "6a321771xxxxxx1312bfebd8",
  "cwd": "/Users/example/Documents/code/hooks_test",
  "hook_event_name": "UserPromptSubmit",
  "workspace_roots": [
    "/Users/example/Documents/code/hooks_test"
  ],
  "agent_id": "dev_agent",
  "agent_type": "dev_agent",
  "prompt": "介绍当前项目",
  "email": "zhangsan@example.com",
  "model": "auto/Trae"
}
{
  "session_id": "6a321771xxxxxx1312bfebd8",
  "cwd": "/Users/example/Documents/code/hooks_test",
  "hook_event_name": "PreToolUse",
  "workspace_roots": [
    "/Users/example/Documents/code/hooks_test"
  ],
  "agent_id": "dev_agent",
  "agent_type": "dev_agent",
  "tool_use_id": "call_rDEJn2bhxxxxxx78d9O8Jb7e",
  "tool_name": "LS",
  "llm_tool_name": "LS",
  "tool_input": {
    "path": "/Users/example/Documents/code/hooks_test"
  },
  "email": "zhangsan@example.com",
  "model": "auto/Trae"
}
{
  "session_id": "6a321771xxxxxx1312bfebd8",
  "cwd": "/Users/example/Documents/code/hooks_test",
  "hook_event_name": "PostToolUse",
  "workspace_roots": [
    "/Users/example/Documents/code/hooks_test"
  ],
  "agent_id": "dev_agent",
  "agent_type": "dev_agent",
  "tool_use_id": "call_fNyUXHCOCsxxxxxx4YUtPMdc",
  "tool_name": "Glob",
  "llm_tool_name": "Glob",
  "tool_input": {
    "path": "/Users/example/Documents/code/hooks_test",
    "pattern": "**/*"
  },
  "tool_response": {
    "reach_limit": false,
    "files": [
      {
        "absPath": "/Users/example/Documents/code/hooks_test/hooks_server.py",
        "size": 3755,
        "mtime": 1781665658642,
        "lineCount": null
      }
    ]
  },
  "email": "zhangsan@example.com",
  "model": "auto/Trae"
}
{
  "session_id": "6a321771xxxxxx1312bfebd8",
  "cwd": "/Users/example/Documents/code/hooks_test",
  "hook_event_name": "Stop",
  "workspace_roots": [
    "/Users/example/Documents/code/hooks_test"
  ],
  "agent_id": "dev_agent",
  "agent_type": "dev_agent",
  "stop_hook_active": false,
  "loop_count": 0,
  "last_assistant_message": "项目介绍",
  "email": "zhangsan@example.com",
  "model": "auto/Trae"
}
{
  "session_id": "6a321771xxxxxx1312bfebd8",
  "cwd": "/Users/example/Documents/code/hooks_test",
  "hook_event_name": "Notification",
  "workspace_roots": [
    "/Users/example/Documents/code/hooks_test"
  ],
  "agent_id": "dev_agent",
  "agent_type": "dev_agent",
  "notification_type": "idle_prompt",
  "message": "智能体已完成任务",
  "email": "zhangsan@example.com",
  "model": "auto/Trae"
}

服务端点

这段示例代码提供了一个可在本地运行的 TRAE 企业 HTTP Hook 调试服务。服务启动后会监听 9000 端口,并为 SessionStartUserPromptSubmitPreToolUsePostToolUseStopNotification 这些 Hook 事件分别提供 HTTP endpoint。TRAE 触发 Hook 时,会将事件请求体发送到对应 endpoint;服务端会在终端打印完整的请求体和响应体,便于你检查字段结构、事件触发时机和流程控制结果。你可以直接使用该示例验证企业 Hook 配置是否生效,也可以基于它扩展企业内部的权限校验、审计记录、上下文注入或通知逻辑。

#!/usr/bin/env python3
import json
import sys
from copy import deepcopy
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse

PORT = 9000

ROUTES = {
    "/session-start": {
        "event": "SessionStart",
        "description": "会话开始时触发,默认继续执行。",
    },
    "/user-prompt-submit": {
        "event": "UserPromptSubmit",
        "description": "用户提交 Prompt 后触发,默认继续执行。",
    },
    "/user-prompt-submit/block": {
        "event": "UserPromptSubmit",
        "action": "block",
        "description": "示例:阻断用户请求。",
    },
    "/pre-tool-use": {
        "event": "PreToolUse",
        "description": "工具执行前触发,默认继续执行。",
    },
    "/pre-tool-use/block-read": {
        "event": "PreToolUse",
        "action": "block_read",
        "description": "示例:阻断 Read 工具。",
    },
    "/pre-tool-use/ask": {
        "event": "PreToolUse",
        "action": "ask",
        "description": "示例:要求用户确认后再执行工具。",
    },
    "/post-tool-use": {
        "event": "PostToolUse",
        "description": "工具执行后触发,默认继续执行。",
    },
    "/stop": {
        "event": "Stop",
        "description": "智能体准备结束当前查询时触发,默认允许停止。",
    },
    "/stop/continue": {
        "event": "Stop",
        "action": "continue",
        "description": "示例:阻止智能体停止,并要求继续处理。",
    },
    "/notification": {
        "event": "Notification",
        "description": "通知事件,非阻断,响应体不影响智能体流程。",
    },
}

DEFAULT_RESPONSES = {
    "SessionStart": {},
    "UserPromptSubmit": {},
    "PreToolUse": {},
    "PostToolUse": {},
    "Stop": {},
    "Notification": {},
}


def json_dumps(data: dict) -> str:
    return json.dumps(data, indent=2, ensure_ascii=False)


def print_event(event_name: str, path: str, payload: dict, response: dict) -> None:
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
    sep = "─" * 80

    print(f"\n{'━' * 80}")
    print(f"  TRAE Hook Event: {event_name}")
    print(f"  Time: {ts}")
    print(f"  POST http://localhost:{PORT}{path}")
    print(sep)
    print("[request]")
    print(json_dumps(payload))
    print(sep)
    print("[response]")
    print(json_dumps(response))
    print(sep)
    sys.stdout.flush()


def build_response(route: dict, payload: dict) -> dict:
    event_name = route["event"]
    action = route.get("action")

    if action == "block":
        return {
            "decision": "block",
            "reason": "该请求被企业 Hook 策略阻断。",
        }

    if action == "block_read":
        tool_name = payload.get("tool_name")
        if tool_name == "Read":
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": "Read 工具已被企业 Hook 策略阻断。",
                }
            }

        return {}

    if action == "ask":
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "ask",
                "permissionDecisionReason": "该工具调用需要用户确认后再执行。",
            }
        }

    if action == "continue":
        return {
            "decision": "block",
            "reason": "请继续检查当前回答是否完整,并补充必要的验证结果。",
        }

    return deepcopy(DEFAULT_RESPONSES[event_name])


class HookHandler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        return

    def send_json(self, data: dict, status: int = 200) -> None:
        body = json.dumps(data, ensure_ascii=False).encode("utf-8")

        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        path = urlparse(self.path).path

        if path == "/":
            self.send_json(
                {
                    "status": "ok",
                    "name": "TRAE Enterprise HTTP Hooks Server",
                    "port": PORT,
                    "routes": ROUTES,
                }
            )
            return

        self.send_json({"error": f"not found: {path}"}, status=404)

    def do_POST(self):
        path = urlparse(self.path).path
        route = ROUTES.get(path)

        if route is None:
            self.send_json({"error": f"unknown hook path: {path}"}, status=404)
            return

        length = int(self.headers.get("Content-Length", 0))
        raw = self.rfile.read(length) if length else b"{}"

        try:
            payload = json.loads(raw.decode("utf-8"))
        except json.JSONDecodeError as exc:
            self.send_json(
                {
                    "error": "invalid JSON request body",
                    "detail": str(exc),
                },
                status=400,
            )
            return

        response = build_response(route, payload)
        print_event(route["event"], path, payload, response)
        self.send_json(response)


def main():
    server = HTTPServer(("0.0.0.0", PORT), HookHandler)

    print(f"\nTRAE Enterprise HTTP Hooks Server  port {PORT}")
    print("─" * 80)
    print("Health check:")
    print(f"  GET   http://localhost:{PORT}/")
    print("\nHook endpoints:")
    for path, route in ROUTES.items():
        print(
            f"  POST  http://localhost:{PORT}{path:<32}  "
            f"{route['event']:<16}  {route.get('description', '')}"
        )
    print("─" * 80)
    print("Waiting for TRAE hook events...\n")
    sys.stdout.flush()

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nStopped.")


if __name__ == "__main__":
    main()