Skip to content

Commit 1e68848

Browse files
author
SqlRush
committed
Run local plugin hooks
1 parent b2024d9 commit 1e68848

7 files changed

Lines changed: 205 additions & 27 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
| M10 Agents/tasks/worktree/remote | 已有 Task/TaskOutput/KillTask/SendMessage/TeamCreate/TeamDelete/TeamOutput/TeamSendMessage/TeamDispatch/TeamSchedule/TeamAutoSchedule/TeamCoordinate/ResumeTask/Sleep/Brief/ScheduleCron/RemoteTrigger 入口、sidechain metadata/lifecycle、task progress event、显式与 settings 默认 owned worktree 创建/清理、sparse/symlink settings 应用、`run:true` subagent nested tool loop、agent permission mode/allowlist 过滤,以及 session-scoped team/schedule/remote trigger manifest、daemon heartbeat CLI/state/status/stop/tick/start/restart 控制面审计和跨 session state discovery、remote service manifest 与 `/status show remote` discovery、remote registrationUrl/authToken 注册状态文件、remote poll URL/cursor 与 websocket_url 多帧/tick 消息泵、WebSocket 基础重连/backoff/连接计数审计、callback stream primitive 和 daemon 常驻托管接线、ScheduleCron manual trigger/run_due/turn-start due tick、team coordinator_task_id 元数据、TeamOutput coordinator status、TeamSendMessage target routing、TeamDispatch individualized assignments、TeamSchedule deterministic member assignments、TeamAutoSchedule coordinator briefing + member assignments、coordinator briefing、structured handoff brief、remote trigger injection/event_id dedupe、bridge direct `/remote-trigger`/`/remote-service` HTTP endpoint、WebSocket `remote_trigger`/`remote_status`/`hello`/`health`/`manifest` action 和 remote_trigger/remote_service/websocket_protocol manifest capability;完整 CCR 云端 WebSocket 协议 hardening、多 agent 后台调度循环和模型驱动团队自动调度仍未完成 |
3434
| 全量测试 | 当前 `go test ./...` 通过 |
3535

36-
M4 补充:tool executor 会围绕 `PreToolUse``PostToolUse``PermissionDenied``PermissionRequest` hook 发出 `hook_started``hook_completed``hook_failed``hook_blocked` 进度事件,携带 phase/tool/hook_index 以及阻断、错误、权限行为和 input 更新摘要;`PermissionAsk` 现在走独立 `PermissionRequest` phase 并发出 `permission_requested` 进度,conversation runner 已通过现有 tool progress 通道透出这些事件。settings command/HTTP hook 主路径已接入:支持 matcher/`if` 过滤、JSON stdin/body、stdout/HTTP body JSON `hookSpecificOutput.updatedInput`、exit 2/block、HTTP URL allowlist、HTTP header env allowlist 插值和 `PermissionRequest` allow/deny。plugin/prompt/agent hook、async/background hook 和完整 hook telemetry 仍未完成。
36+
M4 补充:tool executor 会围绕 `PreToolUse``PostToolUse``PermissionDenied``PermissionRequest` hook 发出 `hook_started``hook_completed``hook_failed``hook_blocked` 进度事件,携带 phase/tool/hook_index 以及阻断、错误、权限行为和 input 更新摘要;`PermissionAsk` 现在走独立 `PermissionRequest` phase 并发出 `permission_requested` 进度,conversation runner 已通过现有 tool progress 通道透出这些事件。settings/local-plugin command/HTTP hook 主路径已接入:支持 matcher/`if` 过滤、JSON stdin/body、stdout/HTTP body JSON `hookSpecificOutput.updatedInput`、exit 2/block、HTTP URL allowlist、HTTP header env allowlist 插值和 `PermissionRequest` allow/deny。prompt/agent hook、async/background hook 和完整 hook telemetry 仍未完成。
3737

3838
M10 补充:plugin command/agent 的 allowed tool frontmatter 解析现在只在顶层逗号或空白处分隔,保留括号、方括号和引号内的逗号/空白,避免 `Bash(git commit -m "x,y")` 这类 tool pattern 被误拆。
3939

@@ -231,13 +231,13 @@ M8/M6 补充:`/config`、`/plugin`、`/memory` built-in local-jsx command 现
231231

232232
M8 补充:新增 `internal/plugins` 本地 manifest loader 地基,支持从 cwd 向上到 git root/home 发现 `.claude/plugins/<plugin>/plugin.json`,解析基础 plugin metadata、prompt command、local/local-jsx command metadata 和 manifest 指向的 `SKILL.md` skill,并接入 command registry 的 plugin source 顺序;当前只读加载本地 manifest,不包含 marketplace、install/cache/update、hooks/agents/MCP plugin 激活。
233233

234-
M8 补充:`/plugin list|status` headless/local summary 现在会复用本地 plugin manifest loader,显示发现到的 local plugin manifest 数量、名称/版本,以及 registry 中已注册的 plugin command、skill、agent、MCP server、output style、hook event/hook count 列表;`/plugin show <name>` 会只读展示单个本地 plugin manifest 的路径、启停状态和 commands/skills/agents/MCP/output-style/hooks 明细;`/plugin search <query>` 会搜索本地 plugin metadata、commands、skills、agents、MCP server、output style 和 hook event,并标注 enabled/disabled 状态。仍不执行 marketplace/install/update,也不激活 plugin hooks/agents/MCP。
234+
M8 补充:`/plugin list|status` headless/local summary 现在会复用本地 plugin manifest loader,显示发现到的 local plugin manifest 数量、名称/版本,以及 registry 中已注册的 plugin command、skill、agent、MCP server、output style、hook event/hook count 列表;`/plugin show <name>` 会只读展示单个本地 plugin manifest 的路径、启停状态和 commands/skills/agents/MCP/output-style/hooks 明细;`/plugin search <query>` 会搜索本地 plugin metadata、commands、skills、agents、MCP server、output style 和 hook event,并标注 enabled/disabled 状态。本地 plugin hooks 会进入同步工具 hook executor;仍不执行 marketplace/install/update,也不激活 plugin agents/MCP。
235235

236236
M8 补充:`/plugin marketplaces` 现在会只读列出 settings 中的 extra/strict/blocked marketplace 来源,`/plugin config <name>` 会展示 plugin config option keys、MCP server config names 和 legacy settings keys 且不泄露配置值;真实 marketplace 浏览、install/update/cache lifecycle 仍未完成。
237237

238238
M8/M9 补充:本地 plugin manifest 现在可声明 `mcpServers`/`mcp_servers`,并支持默认 `.mcp.json`、manifest path、array 和 inline server map 形态;`LoadMCPConfigFromSettingsFiles` 会把 cwd 发现到的 plugin MCP servers 传入 runner;configured MCP toolset merge 会对 plugin servers 做手工配置同名/同签名去重,并继续套用现有 MCP allow/deny policy,`/mcp list` 也会显示 plugin MCP server。完整 plugin MCP lifecycle、MCPB 下载/提取、启停 UI、marketplace 安装来源和 health check 仍未完成。
239239

240-
M8 补充:本地 plugin manifest loader 现在会只读发现默认 `agents/`、manifest `agents` 额外 markdown 文件/目录、默认 `hooks/hooks.json` 和 manifest `hooks` inline/path 配置,并在 `/plugin list|status` headless summary 中显示 plugin command、skill、agent、MCP server 和 hook event/hook count;这些 hooks/agents 目前仅作为 manifest 元数据暴露,尚未接入 agent runtime 或 hook executor
240+
M8 补充:本地 plugin manifest loader 现在会发现默认 `agents/`、manifest `agents` 额外 markdown 文件/目录、默认 `hooks/hooks.json` 和 manifest `hooks` inline/path 配置,并在 `/plugin list|status` headless summary 中显示 plugin command、skill、agent、MCP server 和 hook event/hook count;这些 hooks 已接入同步工具 hook executor,agents 仍仅作为 manifest 元数据暴露,尚未接入 agent runtime。
241241

242242
M8 补充:本地 plugin prompt command discovery 现在除 manifest command object 外,也支持默认 `commands/` markdown 目录、manifest `commands` path/path-array 形态,以及基础 object-mapping `source`/`content` metadata,按 plugin 名称生成 `plugin:path:name` 命名空间并复用现有 prompt expansion/transcript/slash command 管线;prompt expansion 会从 `pluginConfigs[plugin].options`/legacy `plugins[plugin]` 注入并替换 `${user_config.key}``$user_config.key``{{ user_config.key }}`。shell expansion、完整 metadata 细节和 marketplace command 来源仍未完成。
243243

@@ -1427,9 +1427,9 @@ M7 补充:terminal input parser 和 configurable keybinding name parser 现在
14271427
- local commands、local-jsx command abstraction。
14281428
- bundled/user/plugin/MCP skills discovery。
14291429
- plugin manifest、marketplace、install/cache/update。
1430-
- plugin hooks/agents/MCP。
1430+
- plugin hooks/agents/MCP,其中本地 plugin 同步工具 hook 已接入,剩余完整 plugin agent/MCP 与 hook UI/policy parity
14311431

1432-
当前状态:已完成项目 skill discovery、目录式 `SKILL.md` prompt metadata loading、project legacy `.claude/commands` prompt command loading、command registry metadata/lookup/filter、部分内置 slash command aliases/metadata、prompt expansion、基础 `Skill` tool inline 调用、本地项目 prompt skill 的基础 slash 调用接入、本地 prompt skill 的 command permissions attachment/current-turn 权限继承,本地 plugin command/skill/agent/MCP server/output style/hook 的只读 manifest discovery,headless `/help`/`/skills` 列表与单项详情,output style 系统提示注入,以及 `/clear` 基础 local command no-query 路径;仍缺 bundled/MCP/remote skills、forked skill/agent 执行、完整 local/local-jsx 实际执行、TUI `/help`/`/skills` 面板、权限 UI/SDK 展示、plugin marketplace/cache/update、skill prompt shell injection 和完整 hooks/agents/MCP/output-style UI 接线。
1432+
当前状态:已完成项目 skill discovery、目录式 `SKILL.md` prompt metadata loading、project legacy `.claude/commands` prompt command loading、command registry metadata/lookup/filter、部分内置 slash command aliases/metadata、prompt expansion、基础 `Skill` tool inline 调用、本地项目 prompt skill 的基础 slash 调用接入、本地 prompt skill 的 command permissions attachment/current-turn 权限继承,本地 plugin command/skill/agent/MCP server/output style/hook manifest discovery,本地 plugin 同步工具 hook 执行,headless `/help`/`/skills` 列表与单项详情,output style 系统提示注入,以及 `/clear` 基础 local command no-query 路径;仍缺 bundled/MCP/remote skills、forked skill/agent 执行、完整 local/local-jsx 实际执行、TUI `/help`/`/skills` 面板、权限 UI/SDK 展示、plugin marketplace/cache/update、skill prompt shell injection 和完整 agents/MCP/output-style UI 接线。
14331433

14341434
### M9: MCP Platform
14351435

docs/first-second-parity-audit.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ Anthropic API 和 conversation:
8585
- OAuth support now includes production OAuth config, scope parsing, Claude.ai scope detection, auth URL construction, PKCE verifier/challenge, state generation, and expiry checks.
8686
- Session/history support now includes CC-compatible prompt history references, pasted text/image placeholder parsing, paste-cache hashing and retrieval, `history.jsonl` append/load, current-session-first up-arrow ordering, ctrl+r-style deduped timestamped history, `CLAUDE_CODE_SKIP_PROMPT_HISTORY`, remote session event pagination helpers, lenient transcript loading, legacy progress parent-bridge recovery, compact-boundary pruning, snip removal/relink replay, metadata entry collection, leaf UUID calculation, conversation-chain reconstruction, orphaned parallel tool-result recovery, content-replacement record loading/reconstruction, tombstone metadata delete/relink replay, and tombstone-style transcript message removal with a size guard.
8787
- Anthropic API layer now covers streaming accumulation, usage update/accumulation semantics, non-streaming max token cap, thinking-budget adjustment, retry/backoff with `Retry-After` and `x-should-retry`, context-overflow `max_tokens` retry adjustment, beta-header dedupe, custom request headers, basic prompt cache breakpoint/cache-reference/cache-edits placement, prompt dump JSONL capture for init/new user messages/non-streaming responses/stream chunks, and CC-compatible USD cost calculation for known Claude models including cache read/write, web search requests, and Opus 4.6 fast-tier pricing.
88-
- Tool runtime now includes concurrency partitioning, ordered concurrent execution, interrupt behavior/defaults, max result size metadata, oversized result persistence, pre/post/permission-denied/permission-request hook dispatch, settings command-backed and HTTP hook execution for the synchronous tool-hook path, hook-driven input updates/blocking/permission-request allow-deny, executor hook phase progress events, lifecycle progress events, and pre-call cancellation checks.
88+
- Tool runtime now includes concurrency partitioning, ordered concurrent execution, interrupt behavior/defaults, max result size metadata, oversized result persistence, pre/post/permission-denied/permission-request hook dispatch, settings and local-plugin command-backed/HTTP hook execution for the synchronous tool-hook path, hook-driven input updates/blocking/permission-request allow-deny, executor hook phase progress events, lifecycle progress events, and pre-call cancellation checks.
8989
- Conversation runner can now use streaming clients, aggregate stream events into assistant messages, run tool calls through the orchestrator, preserve transcript append behavior, and apply CC-style per-message aggregate tool-result budget replacement before API requests with persisted replacement records for resume.
9090

9191
## Still Missing For 100% Compatibility
9292

9393
The following items remain incomplete and must not be treated as done:
9494

95-
- Full permission hook flow gaps that remain: plugin/prompt/agent hook execution, async/background hook runtime policy, richer hook telemetry, and full UI/SDK surfacing beyond the current executor hook phase progress events.
95+
- Full permission hook flow gaps that remain: prompt/agent hook execution, async/background hook runtime policy, richer hook telemetry, and full UI/SDK surfacing beyond the current executor hook phase progress events.
9696
- Auto mode / YOLO classifier: transcript construction, two-stage classifier, XML/tool-use parsing, prompt dump, denial circuit breaker, model gating, and fallback behavior.
9797
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
9898
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, complete local/local-jsx slash-command execution wiring, command permission UI/SDK surfacing, forked/remote/MCP/plugin/bundled SkillTool behavior, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.

internal/conversation/run.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3471,6 +3471,27 @@ func (r Runner) outputStylePlugins() []pluginpkg.LoadedPlugin {
34713471
return pluginpkg.LoadPluginDirsWithSettings(pluginpkg.ProjectPluginDirs(r.WorkingDirectory), r.mergedSettings())
34723472
}
34733473

3474+
func (r Runner) pluginToolHooks(settings contracts.Settings) []tool.Hook {
3475+
if strings.TrimSpace(r.WorkingDirectory) == "" {
3476+
return nil
3477+
}
3478+
if settings.DisableAllHooks != nil && *settings.DisableAllHooks {
3479+
return nil
3480+
}
3481+
if settings.AllowManagedHooksOnly != nil && *settings.AllowManagedHooksOnly {
3482+
return nil
3483+
}
3484+
options := hookpkg.Options{
3485+
AllowedHTTPHookURLs: settings.AllowedHTTPHookURLs,
3486+
HTTPHookAllowedEnvVars: settings.HTTPHookAllowedEnvVars,
3487+
}
3488+
var out []tool.Hook
3489+
for _, plugin := range pluginpkg.LoadPluginDirsWithSettings(pluginpkg.ProjectPluginDirs(r.WorkingDirectory), settings) {
3490+
out = append(out, hookpkg.FromRaw(plugin.Hooks, options)...)
3491+
}
3492+
return out
3493+
}
3494+
34743495
func settingsPermissionsSummary(setting *contracts.PermissionsSetting) string {
34753496
if setting == nil {
34763497
return "none"
@@ -5289,7 +5310,9 @@ func (r Runner) executeToolUses(ctx context.Context, uses []contracts.ToolUse, m
52895310
return nil
52905311
})
52915312
executor := r.Tools
5292-
executor.Hooks = append(executor.Hooks, hookpkg.FromSettings(r.mergedSettings())...)
5313+
settings := r.mergedSettings()
5314+
executor.Hooks = append(executor.Hooks, hookpkg.FromSettings(settings)...)
5315+
executor.Hooks = append(executor.Hooks, r.pluginToolHooks(settings)...)
52935316
for update := range tool.RunTools(toolCtx, executor, uses, progressSink, tool.RunOptions{}) {
52945317
use := update.ToolUse
52955318
result := update.Result

internal/conversation/run_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,90 @@ func TestRunnerExecutesSettingsCommandHookForToolUse(t *testing.T) {
302302
}
303303
}
304304

305+
func TestRunnerExecutesPluginCommandHookForToolUse(t *testing.T) {
306+
registry, err := tool.NewRegistry(namedTextTool("Echo", "echo:"))
307+
if err != nil {
308+
t.Fatal(err)
309+
}
310+
repo := filepath.Join(t.TempDir(), "repo")
311+
cwd := filepath.Join(repo, "pkg")
312+
pluginDir := filepath.Join(repo, ".claude", "plugins", "demo")
313+
if err := os.MkdirAll(filepath.Join(repo, ".git"), 0o755); err != nil {
314+
t.Fatal(err)
315+
}
316+
if err := os.MkdirAll(filepath.Join(pluginDir, "hooks"), 0o755); err != nil {
317+
t.Fatal(err)
318+
}
319+
if err := os.MkdirAll(cwd, 0o755); err != nil {
320+
t.Fatal(err)
321+
}
322+
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(`{"name":"demo","version":"1.0.0"}`), 0o644); err != nil {
323+
t.Fatal(err)
324+
}
325+
if err := os.WriteFile(filepath.Join(pluginDir, "hooks", "hooks.json"), []byte(`{
326+
"hooks": {
327+
"PreToolUse": [{
328+
"matcher": "Echo",
329+
"hooks": [{
330+
"type": "command",
331+
"command": "printf '%s\n' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"updatedInput\":{\"text\":\"from-plugin\"}},\"systemMessage\":\"plugin updated\"}'"
332+
}]
333+
}]
334+
}
335+
}`), 0o644); err != nil {
336+
t.Fatal(err)
337+
}
338+
client := &fakeClient{calls: []fakeCall{
339+
{response: &anthropic.Response{
340+
ID: "msg_tool",
341+
Type: "message",
342+
Role: "assistant",
343+
Model: "sonnet",
344+
StopReason: "tool_use",
345+
Content: []contracts.ContentBlock{{
346+
Type: contracts.ContentToolUse,
347+
ID: "toolu_plugin_hook",
348+
Name: "Echo",
349+
Input: json.RawMessage(`{"text":"original"}`),
350+
}},
351+
}},
352+
{response: &anthropic.Response{
353+
ID: "msg_done",
354+
Type: "message",
355+
Role: "assistant",
356+
Model: "sonnet",
357+
StopReason: "end_turn",
358+
Content: []contracts.ContentBlock{contracts.NewTextBlock("done")},
359+
}},
360+
}}
361+
var progress []contracts.ToolProgress
362+
runner := Runner{
363+
Client: client,
364+
Tools: tool.NewExecutor(registry),
365+
Model: "sonnet",
366+
MaxTokens: 128,
367+
SessionID: "sess_plugin_hook",
368+
SessionPath: filepath.Join(t.TempDir(), "session.jsonl"),
369+
WorkingDirectory: cwd,
370+
MCP: &MCPConfig{},
371+
OnEvent: func(event Event) {
372+
if event.Type == EventToolProgress && event.ToolProgress != nil {
373+
progress = append(progress, *event.ToolProgress)
374+
}
375+
},
376+
}
377+
result, err := runner.RunTurn(context.Background(), nil, messages.UserText("run echo"))
378+
if err != nil {
379+
t.Fatal(err)
380+
}
381+
if len(result.ToolResults) != 1 || result.ToolResults[0].Content != "echo:from-plugin" {
382+
t.Fatalf("tool results = %#v", result.ToolResults)
383+
}
384+
if !hasHookProgress(progress, tool.HookPreToolUse, "hook_completed") {
385+
t.Fatalf("hook progress = %#v", progress)
386+
}
387+
}
388+
305389
func TestRunnerRunsDueSchedulesBeforeMainRequest(t *testing.T) {
306390
transcriptPath := filepath.Join(t.TempDir(), "session.jsonl")
307391
sessionID := contracts.ID("sess_schedule_tick")

0 commit comments

Comments
 (0)