Skip to content

Commit 12a1423

Browse files
committed
feat(skills): execute session-scoped command hooks
1 parent f3b4331 commit 12a1423

41 files changed

Lines changed: 1973 additions & 86 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/architecture/extensions/external-ai-work-sources-design.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,14 @@ Plugin Host Runtime、已退役的 LSP Runtime,以及通用动态模型路由
479479
Claude runtime 变量与动态 shell 表达式保留为未展开、未执行的文本,加载说明明确它们不是实际值或命令结果;
480480
如任务需要这些数据,Agent 必须通过当前工作区的正常工具与权限流程获取。行内 shell 表达式的识别要求行首或空白边界及闭合
481481
反引号;普通 Markdown 中的感叹号、Excel `Sheet1!A1` 和错误值不会触发兼容性警告。
482-
`context`(包括 `fork`)、`agent`、`hooks`、`paths`、`shell`、`runtime`、`background`、`disallowed-tools`
482+
`context`(包括 `fork`)、`agent`、`paths`、`shell`、`runtime`、`background`、`disallowed-tools`
483483
涉及未实现的执行方式或约束,仍阻止加载;格式损坏及无效调用控制字段也仍返回错误。本地与 Remote、名称与稳定键加载共享
484484
同一兼容性判断和模型说明。此切片不增加插件 Skill、祖先活动目录、文件 watcher、URL 来源或另一条 reload 命令。
485+
- Claude Skill 的同步 command `hooks` 在显式 Skill 调用时注册到会话的共享 HookRegistry,扫描与导入不执行。
486+
Skill 调用形成工具预检边界,同轮后续工具也接受新增 Hook;Bash/ExecCommand 与 Write 参数由方言适配转换,
487+
ask 进入现有权限邮箱,deny/exit 2 阻断,updatedInput 继续接受原参数约束和最终校验。
488+
once、会话隔离、幂等注册与退出取消由现有 portable Hook 引擎持有;配置 gating 与生命周期清理在 Core owner。
489+
Remote 工作区激活明确不支持,其他控制面复用目标 runtime;详见 [Agent Hooks](../../features/agent-hooks.zh-CN.md)。
485490
- Claude Subagent 扫描用户与逐层项目 `.claude/agents/**/*.md`,近工作目录定义整项覆盖;Claude MCP 保留
486491
`local > project > user` 的整项覆盖,local 只读取与规范化当前工作区严格匹配的项目项。
487492
- Codex Subagent 从用户与逐层项目 `[agents]`、角色文件合并,缺失字段按 Codex 层级继承;`enabled`、默认模型、角色级

‎docs/features/agent-hooks.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,48 @@ the same backend and keeps `/hooks_external` and `/hooks-external` as aliases
179179
for the unified `/hooks` management view. `reset` is available only as explicit
180180
recovery for a corrupt OpenBitFun-managed index and never changes source files.
181181

182+
## Hooks declared by skills
183+
184+
Invoking a Claude-format skill through `Skill` registers its validated synchronous
185+
`type: "command"` handlers in the existing Hook engine for that session. Discovery,
186+
listing, and importing do not register or execute them. Imported Claude skills
187+
retain their source dialect. The external Hook catalog remains read-only.
188+
189+
Skill hooks use the supported lifecycle events listed above, regular-expression
190+
matchers, stdin JSON, exit-code blocking, and `updatedInput`. They run after the
191+
configured command layers. Registration is idempotent; invoking a changed hook
192+
declaration in the same session returns an error instead of replacing active rules.
193+
`once: true` is consumed after exit code 0, atomically across concurrent dispatches;
194+
exit 2, other failures, and timeouts leave it eligible. A skill loaded mid-batch is
195+
a preflight barrier: later calls see its hooks even within the same model response.
196+
197+
The Claude adapter maps `Bash` to `ExecCommand` and `command` to `cmd`. For `Write`,
198+
it translates the path-first `payload` into `file_path`/`content` and converts
199+
`updatedInput` back before normal input validation. An ambiguous Write destination
200+
is blocked while a matching skill hook is active. `Edit` keeps its existing fields.
201+
The command receives `CLAUDE_SKILL_DIR`, `CLAUDE_SESSION_ID`, and
202+
`CLAUDE_PROJECT_DIR`; this does not expand variables in the skill's prose.
203+
204+
Skill `PreToolUse` hooks also support Claude's `permissionDecision: "ask"` through
205+
the existing session permission mailbox. An ask requires a fresh reply even in
206+
bypass mode; a policy deny still wins. The hook reason is included in the approval
207+
metadata. Native `hooks.json` keeps its Codex decision contract.
208+
209+
The master `app.hooks.enabled` gate applies. Project skills additionally require
210+
`app.hooks.project_hooks_enabled`, including on subsequent dispatch. Activation
211+
without a session/local workspace, or in an SSH/remote workspace, returns an
212+
explicit error; no controller-local fallback executes. Remote control, Peer Device,
213+
and Detached Dispatch reuse their target runtime's session and permission owners;
214+
this change does not introduce a separate client-side hook runner.
215+
216+
Registrations survive ordinary turns and idle in-process session unloading. Session
217+
end/delete/discard removes them and cancels running handlers; cancelled SessionEnd
218+
dispatch also clears them. They are process-local and are not restored after a
219+
runtime restart: invoke the skill again. Unknown events, asynchronous handlers,
220+
`prompt`/`agent` handlers, and unknown execution fields reject the entire skill
221+
rather than silently dropping constraints. Script files remain live dependencies,
222+
as for native command hooks; the declaration fingerprint does not snapshot scripts.
223+
182224
## Quick start
183225

184226
Create `<user config dir>/config/hooks.json`:

‎docs/features/agent-hooks.zh-CN.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,38 @@ openbitfun hooks reset <user|project> --confirm
144144
`/hooks_external`、`/hooks-external` 作为统一 `/hooks` 管理视图的兼容别名。
145145
`reset` 只用于显式恢复损坏的 OpenBitFun 托管索引,绝不会修改来源文件。
146146

147+
## 技能中的 Hooks
148+
149+
通过 `Skill` 调用 Claude 格式技能时,经过完整校验的同步 `type: command`
150+
处理器会注册到现有 Hook 引擎,归属于当前会话。扫描、列表和导入不注册、不执行;
151+
导入后的 Claude 技能保留来源方言,外部 Hook 目录仍然只是只读发现数据。
152+
153+
技能使用上文已支持的生命周期事件、正则 matcher、stdin JSON、退出码阻断和
154+
`updatedInput`。执行顺序在配置的 command 层之后。同一会话重复加载不重复注册;
155+
声明改变时明确报错,不静默替换已激活规则。`once: true` 仅在退出码为 0 后消费,
156+
并发派发也只成功执行一次;退出码 2、其他失败和超时不消费。
157+
同一模型响应先调用技能、再调用工具时,后续工具会等技能完成后再执行 Hook 与权限预检。
158+
159+
Claude 适配将 `Bash` 匹配到 `ExecCommand`,并双向转换 `command/cmd`。
160+
`Write` 的路径前缀 `payload` 会转换成 `file_path/content`,返回的 `updatedInput`
161+
再转换回本地格式并接受正常校验;有匹配的技能 Hook 时,无法确定写入目标的调用会被阻断。
162+
`Edit` 沿用原参数。命令环境包含 `CLAUDE_SKILL_DIR`、`CLAUDE_SESSION_ID`、
163+
`CLAUDE_PROJECT_DIR`;技能正文中的变量仍不会因此展开。
164+
165+
技能 `PreToolUse` 的 `permissionDecision: ask` 进入现有会话权限邮箱,携带 Hook
166+
原因;即使启用了自动批准,也需要用户本次答复,已有权限拒绝仍然优先。
167+
原生 `hooks.json` 保持 Codex 决策契约。
168+
169+
激活受 `app.hooks.enabled` 控制;项目技能还要求 `app.hooks.project_hooks_enabled`,
170+
后续派发也遵守该开关。缺少所属会话、本地工作区或处于 SSH/远程工作区时明确拒绝激活,
171+
不会回退到控制端本地执行。远程控制、Peer Device、Detached Dispatch 复用目标运行时
172+
已有的会话和权限 owner,不增加客户端执行器。
173+
174+
注册跨普通回合及进程内空闲卸载保留;会话结束、删除或临时会话丢弃时清除并取消正在运行的
175+
处理器,SessionEnd 派发被取消时也会清理。状态只存在于运行时内存,进程重启后需要重新调用技能。
176+
未知事件、异步、`prompt/agent` 类型和未知执行字段会使整份技能加载失败,不会只丢弃部分约束。
177+
脚本文件与原生命令 Hook 一样是实时依赖,声明指纹不代表脚本快照。
178+
147179
## 快速开始
148180

149181
创建 `<用户配置目录>/config/hooks.json`:

‎scripts/core-boundaries/cargo-dependency-boundaries.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ const CORE_TOKIO_AGGREGATES = new Set([
199199
'tools-mcp',
200200
]);
201201
const AGENT_RUNTIME_TOKIO_FEATURES = new Map([
202-
['native-hook-runtime', ['io-util', 'macros', 'process', 'rt', 'time']],
202+
['native-hook-runtime', ['io-util', 'macros', 'process', 'rt', 'sync', 'time']],
203203
['agent-runtime', ['io-util', 'macros', 'process', 'rt', 'sync', 'time']],
204204
]);
205205

‎scripts/core-boundaries/rules/feature-rules.mjs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const optionalDependencyFeatureOwnerRules = [
146146
{ depName: 'log', ownerFeatures: ['agent-runtime', 'native-hook-runtime'] },
147147
{ depName: 'regex', ownerFeatures: ['agent-runtime', 'definition-contracts', 'native-hook-settings'] },
148148
{ depName: 'serde', ownerFeatures: ['agent-runtime', 'definition-contracts', 'native-hook-runtime'] },
149-
{ depName: 'serde_json', ownerFeatures: ['agent-runtime', 'native-hook-runtime', 'native-hook-settings'] },
149+
{ depName: 'serde_json', ownerFeatures: ['agent-runtime', 'definition-contracts', 'native-hook-runtime', 'native-hook-settings'] },
150150
{ depName: 'serde_yaml', ownerFeatures: ['agent-runtime', 'definition-contracts'] },
151151
{ depName: 'sha2', ownerFeatures: ['agent-runtime'] },
152152
{ depName: 'thiserror', ownerFeatures: ['agent-runtime', 'definition-contracts', 'native-hook-runtime'] },
@@ -604,9 +604,12 @@ export const capabilityContractDependencyRules = [
604604
featureProfiles: {
605605
default: [],
606606
'definition-contracts': [
607+
// Skill declarations reuse pure hook validation without process support.
608+
'native-hook-settings',
607609
'dep:openbitfun-core-types',
608610
'dep:regex',
609611
'dep:serde',
612+
'dep:serde_json',
610613
'dep:serde_yaml',
611614
'dep:thiserror',
612615
],
@@ -623,6 +626,8 @@ export const capabilityContractDependencyRules = [
623626
'tokio/macros',
624627
'tokio/process',
625628
'tokio/rt',
629+
// Session hook once guards and cancellation watchers.
630+
'tokio/sync',
626631
'tokio/time',
627632
],
628633
'agent-runtime': [

‎src/apps/desktop/src/api/skill_api.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,7 @@ mod tests {
16041604
path: "/remote/denied".into(),
16051605
source_id: "codex".into(),
16061606
message: "permission denied".into(),
1607+
unsupported_field: None,
16071608
},
16081609
],
16091610
};

‎src/crates/assembly/core/AGENTS.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ Skill discovery, installation provenance, and local/remote registry regressions:
283283
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib agentic::tools::implementations::skills::
284284
```
285285

286+
Skill hook activation, session cleanup, and tool preflight/permission ordering:
287+
288+
```bash
289+
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib native_hooks
290+
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib agentic::tools::pipeline::tool_pipeline::tests
291+
```
292+
286293
For configured OpenCode discovery and explicit skill loading, include their owner feature and tool tests:
287294

288295
```bash

‎src/crates/assembly/core/src/agentic/session/session_manager.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5016,6 +5016,7 @@ impl SessionManager {
50165016
}
50175017
}
50185018

5019+
crate::native_hooks::clear_session_hook_state(session_id);
50195020
self.active_turn_permission_modes.remove(session_id);
50205021
clear_session_runtime_stores(
50215022
session_id,

‎src/crates/assembly/core/src/agentic/tools/framework.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ pub trait Tool: Send + Sync {
111111
self.is_readonly()
112112
}
113113

114+
/// Subsequent calls must be preflighted after this tool finishes because
115+
/// it can change session execution policy (for example, activate hooks).
116+
fn invalidates_tool_preflight(&self) -> bool {
117+
false
118+
}
119+
114120
/// Describe permission actions and resources without performing side effects.
115121
fn permission_intents(
116122
&self,

‎src/crates/assembly/core/src/agentic/tools/implementations/skill_tool.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ impl Tool for SkillTool {
153153
}
154154

155155
fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool {
156+
false
157+
}
158+
159+
fn invalidates_tool_preflight(&self) -> bool {
156160
true
157161
}
158162

@@ -299,6 +303,8 @@ impl Tool for SkillTool {
299303
}
300304
};
301305

306+
crate::native_hooks::activate_skill_hooks(&skill_data, context).await?;
307+
302308
if let Some(arguments) = input.get("arguments").and_then(Value::as_str) {
303309
skill_data.content = expand_prompt_template_arguments_with_names(
304310
&skill_data.content,

0 commit comments

Comments
 (0)