@fyeeme/pi-hooks 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +59 -18
- package/index.ts +6 -6
- package/package.json +52 -52
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.6] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Peer dependency floor raised to `@earendil-works/pi-coding-agent >= 0.99.0`; dev toolchain pinned to 0.99.2 (typecheck and tests pass against 0.99.2 unchanged).
|
|
15
|
+
|
|
16
|
+
### Breaking Changes
|
|
17
|
+
|
|
18
|
+
- The hooks config file is now `hook.json` (was `hooks.json`), in all three locations: `~/.pi/agent/hook.json` (user-global), `<project>/.pi/hook.json`, and the home-directory fallback `~/.pi/hook.json`. Rename your existing config file; `PI_HOOKS_CONFIG` is unaffected.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- README: pi registers MCP tools as `mcp__<server>__<tool>` (same as Claude Code), not `<server>_<tool>` — fixed the matcher guidance and the Serena example config (`serena_.*` → `mcp__serena__.*`); a wrong matcher silently never matched, disabling `auto-approve`.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Ship `pi.yml`, a pi-adapted Serena context (based on upstream `claude-code.yml`): symbol-first steering via `serena start-mcp-server --context pi`, plus trimming of the six Serena tools that duplicate pi built-ins. See README "Recommended: steer at the context level, not only via hooks".
|
|
27
|
+
- Document the `denyAsContext` per-hook flag (shipped in 1.0.4) in the README protocol section.
|
|
28
|
+
|
|
10
29
|
## [1.0.5] - 2026-09-16
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -5,12 +5,14 @@ A Claude Code-compatible hooks runner for [pi](https://pi.dev). Reads your hooks
|
|
|
5
5
|
**Config resolution order** (first file that defines at least one hook wins):
|
|
6
6
|
|
|
7
7
|
1. `PI_HOOKS_CONFIG` env var (exclusive single source when set)
|
|
8
|
-
2. `~/.pi/agent/
|
|
9
|
-
3. `<project>/.pi/
|
|
10
|
-
4. `~/.pi/
|
|
8
|
+
2. `~/.pi/agent/hook.json` — user-global, **top priority**
|
|
9
|
+
3. `<project>/.pi/hook.json` — project-local fallback
|
|
10
|
+
4. `~/.pi/hook.json` — home-directory fallback
|
|
11
11
|
|
|
12
12
|
A file that parses but defines no hooks (e.g. `{}`, or a leftover file in an older schema) does not shadow lower-priority files — the chain falls through. Note the chain is winner-take-all: configs are never merged.
|
|
13
13
|
|
|
14
|
+
> **Breaking change from 1.0.5:** the config file was renamed `hooks.json` → `hook.json` (all three locations). Rename your existing file; `PI_HOOKS_CONFIG` is unaffected.
|
|
15
|
+
|
|
14
16
|
## Install
|
|
15
17
|
|
|
16
18
|
Requires the [pi](https://pi.dev) CLI.
|
|
@@ -48,7 +50,7 @@ See the Pi Packages guide on [pi.dev](https://pi.dev) for the full list of sourc
|
|
|
48
50
|
|
|
49
51
|
## Configuration
|
|
50
52
|
|
|
51
|
-
Create `~/.pi/agent/
|
|
53
|
+
Create `~/.pi/agent/hook.json` (user-global, highest file priority — runs in every project) or `.pi/hook.json` in a project root (used only when no global config defines hooks):
|
|
52
54
|
|
|
53
55
|
```json
|
|
54
56
|
{
|
|
@@ -70,12 +72,13 @@ Create `~/.pi/agent/hooks.json` (user-global, highest file priority — runs in
|
|
|
70
72
|
"hooks": [
|
|
71
73
|
{
|
|
72
74
|
"type": "command",
|
|
73
|
-
"command": "serena-hooks remind --client=claude-code"
|
|
75
|
+
"command": "serena-hooks remind --client=claude-code",
|
|
76
|
+
"denyAsContext": true
|
|
74
77
|
}
|
|
75
78
|
]
|
|
76
79
|
},
|
|
77
80
|
{
|
|
78
|
-
"matcher": "
|
|
81
|
+
"matcher": "mcp__serena__.*",
|
|
79
82
|
"hooks": [
|
|
80
83
|
{
|
|
81
84
|
"type": "command",
|
|
@@ -101,9 +104,9 @@ Create `~/.pi/agent/hooks.json` (user-global, highest file priority — runs in
|
|
|
101
104
|
|
|
102
105
|
## Event Mapping
|
|
103
106
|
|
|
104
|
-
|
|
|
107
|
+
| hook.json event | pi event | Notes |
|
|
105
108
|
|---|---|---|
|
|
106
|
-
| `SessionStart` | `session_start` | Runs
|
|
109
|
+
| `SessionStart` | `session_start` | Runs when a session starts, resumes, forks, or switches — **not** on `reload` (a runtime rebind must not re-run hooks mid-session). `matcher` matches the mapped Claude Code source: `startup` (default), `resume` (resume + fork), `clear` (new session); empty matcher matches all. `additionalContext` is injected into the first user message via the `context` event. |
|
|
107
110
|
| `PreToolUse` | `tool_call` | Runs before each tool. `matcher` is a **regex** against the pi tool name. `additionalContext` is injected before the next LLM call. `permissionDecision: "deny"` or exit code 2 blocks the tool (`terminate: true`; in a single-tool / all-terminating batch this also skips the follow-up LLM call — requires pi >= 0.84.1). |
|
|
108
111
|
| `Stop` | `session_shutdown` | Runs on exit/reload/session switch. Cleanup only — `decision: "block"` is **not** honored (pi cannot prevent exit). Stop hooks are awaited, so a slow hook delays exit up to its `timeout` (default 60s); keep them fast. |
|
|
109
112
|
|
|
@@ -114,9 +117,9 @@ Create `~/.pi/agent/hooks.json` (user-global, highest file priority — runs in
|
|
|
114
117
|
- `""` or `"*"` — match all
|
|
115
118
|
- `"Edit|Write"` — match either
|
|
116
119
|
- `"Notebook.*"` — prefix match
|
|
117
|
-
- `"
|
|
120
|
+
- `"mcp__serena__.*"` — all tools of the `serena` MCP server
|
|
118
121
|
|
|
119
|
-
> **Breaking change from 1.0.x:** matchers were previously interpreted as **globs** (`*`/`?`). If you upgraded, convert patterns like `
|
|
122
|
+
> **Breaking change from 1.0.x:** matchers were previously interpreted as **globs** (`*`/`?`). If you upgraded, convert patterns like `mcp__serena__*` → `mcp__serena__.*`. Invalid regex matches nothing and warns once per pattern (never throws).
|
|
120
123
|
|
|
121
124
|
## Protocol
|
|
122
125
|
|
|
@@ -141,17 +144,20 @@ Commands may return JSON on stdout, or control flow via exit codes:
|
|
|
141
144
|
- exit code **2** (Stop) → ignored (pi cannot block exit).
|
|
142
145
|
- other non-zero → logged, execution continues.
|
|
143
146
|
- non-JSON stdout → logged as a warning, ignored.
|
|
144
|
-
- each hook may set `"timeout"` (seconds, default 60); matching hooks run in **parallel
|
|
147
|
+
- each hook may set `"timeout"` (seconds, default 60); matching hooks run in **parallel** (capped at 8 concurrent).
|
|
148
|
+
- pressing Esc mid-turn kills running hook processes (SIGTERM → SIGKILL escalation, same as the timeout path); an already-aborted turn does not spawn hooks at all.
|
|
149
|
+
- safety caps: hook stdout is capped at 10 MB (the hook is killed beyond that); injected `additionalContext` is capped at 50 KB / 2000 lines with a truncation notice.
|
|
150
|
+
- each hook may set `"denyAsContext": true` — demote a deny (`permissionDecision: "deny"`, or exit 2 carrying a JSON deny payload) to `additionalContext`: the tool call proceeds and the nudge text is injected before the next LLM call (appended to the last user message) instead of blocking. Intended for nudge-style hooks like `serena-hooks remind`, where hard-blocking a read burst stops the agent dead. Injected text: the hook's `additionalContext` if present, otherwise the deny reason.
|
|
145
151
|
|
|
146
152
|
The `additionalContext` is injected into the pi conversation (appended to the last user message, never as a new turn).
|
|
147
153
|
|
|
148
154
|
## MCP Tool Names
|
|
149
155
|
|
|
150
|
-
Pi names MCP tools
|
|
156
|
+
Pi names MCP tools `mcp__<server>__<tool>` (same scheme as Claude Code), so target them with regex like `mcp__serena__.*`. Check your actual tool names with `/mcp` in pi to set the correct `matcher`.
|
|
151
157
|
|
|
152
158
|
## Using pi-hooks with Serena
|
|
153
159
|
|
|
154
|
-
[Serena](https://github.com/oraios/serena) ships a `serena-hooks` CLI (Claude Code compatible) whose four subcommands map cleanly onto pi-hooks events. With Serena's MCP server running in pi (confirm with `/mcp` — you should see a `serena` server), drop this into `~/.pi/agent/
|
|
160
|
+
[Serena](https://github.com/oraios/serena) ships a `serena-hooks` CLI (Claude Code compatible) whose four subcommands map cleanly onto pi-hooks events. With Serena's MCP server running in pi (confirm with `/mcp` — you should see a `serena` server), drop this into `~/.pi/agent/hook.json` (global) or the project's `.pi/hook.json`:
|
|
155
161
|
|
|
156
162
|
```json
|
|
157
163
|
{
|
|
@@ -165,10 +171,16 @@ Pi names MCP tools as `<serverName>_<toolName>` (not `mcp__server__tool` like Cl
|
|
|
165
171
|
"PreToolUse": [
|
|
166
172
|
{
|
|
167
173
|
"matcher": "",
|
|
168
|
-
"hooks": [
|
|
174
|
+
"hooks": [
|
|
175
|
+
{
|
|
176
|
+
"type": "command",
|
|
177
|
+
"command": "serena-hooks remind --client=claude-code",
|
|
178
|
+
"denyAsContext": true
|
|
179
|
+
}
|
|
180
|
+
]
|
|
169
181
|
},
|
|
170
182
|
{
|
|
171
|
-
"matcher": "
|
|
183
|
+
"matcher": "mcp__serena__.*",
|
|
172
184
|
"hooks": [{ "type": "command", "command": "serena-hooks auto-approve --client=claude-code" }]
|
|
173
185
|
}
|
|
174
186
|
],
|
|
@@ -187,16 +199,45 @@ What each hook does:
|
|
|
187
199
|
| Event | Command | Role |
|
|
188
200
|
|---|---|---|
|
|
189
201
|
| `SessionStart` | `activate` | Prompts the agent to activate the project and read Serena's instructions at session start. |
|
|
190
|
-
| `PreToolUse` (`""`) | `remind` | Nudges the agent to prefer Serena's symbolic tools over raw `read`/`grep`. Runs before every tool call. |
|
|
191
|
-
| `PreToolUse` (`
|
|
202
|
+
| `PreToolUse` (`""`) | `remind` | Nudges the agent to prefer Serena's symbolic tools over raw `read`/`grep`. Runs before every tool call. Set `denyAsContext: true` so its deny becomes a context nudge instead of a hard block (recommended). |
|
|
203
|
+
| `PreToolUse` (`mcp__serena__.*`) | `auto-approve` | Auto-approves Serena tool calls while the client is in a permissive permission mode. |
|
|
192
204
|
| `Stop` | `cleanup` | Clears per-session hook state on exit. |
|
|
193
205
|
|
|
194
|
-
**Get the matcher prefix right.** pi
|
|
206
|
+
**Get the matcher prefix right.** pi registers an MCP server's tools as `mcp__<server>__<tool>`. With Serena registered as the `serena` MCP server in `mcp.json` (the default), tools are named `mcp__serena__find_symbol`, `mcp__serena__find_referencing_symbols`, … → use `mcp__serena__.*`. Run `/mcp` in pi to confirm your exact prefix.
|
|
195
207
|
|
|
196
208
|
> ⚠️ **`auto-approve` is currently inert under pi-hooks.** `serena-hooks auto-approve` only emits its approval when stdin reports a permissive `permission_mode` (`acceptEdits` or `auto`), but pi-hooks always sends `permission_mode: "default"` today. The hook still runs but stays silent, so pi's own permission flow applies. `activate`, `remind`, and `cleanup` are unaffected. This will resolve once pi-hooks forwards the real permission mode.
|
|
197
209
|
|
|
198
210
|
`--client=claude-code` is correct for pi: pi-hooks speaks the Claude Code hooks protocol, so Serena treats pi as a Claude Code client.
|
|
199
211
|
|
|
212
|
+
### Recommended: steer at the context level, not only via hooks
|
|
213
|
+
|
|
214
|
+
`serena-hooks remind` is a **fallback**, not the main steering mechanism. Serena gates it behind a burst counter (8 consecutive `read`/`grep` calls in the currently installed release line, 3 in upstream master) plus a 120-second silence window after every nudge — so most `read` calls legitimately produce no output. The primary layer is Serena's **context system**: `serena start-mcp-server --context <name>` injects a behavior-constraining prompt and trims tools that duplicate the host agent's built-ins.
|
|
215
|
+
|
|
216
|
+
This package ships [`pi.yml`](./pi.yml) — a pi-adapted context based on Serena's own `claude-code.yml`: symbol-first read/edit rules written against pi's tool names (`read`/`bash`/`edit`), and `excluded_tools` trimming the six Serena tools that duplicate pi built-ins (`read_file`, `execute_shell_command`, `find_file`, `list_dir`, `search_for_pattern`, `create_text_file`). Install and wire it up:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
cp pi.yml ~/.serena/contexts/ # user contexts dir; a same-named context overrides the built-ins
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```jsonc
|
|
223
|
+
// ~/.pi/agent/mcp.json
|
|
224
|
+
"serena": {
|
|
225
|
+
"type": "stdio",
|
|
226
|
+
"command": "serena",
|
|
227
|
+
"args": ["start-mcp-server", "--project-from-cwd", "--context", "pi"]
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Optionally control how the model reaches Serena's tools with `exposure` / `toolExposure` (see pi's MCP docs for 0.99+ semantics): `"exposure": "codemode"` has the model batch Serena calls inside codemode scripts, `"exposure": "deferred"` loads them one by one via `tool_search`, and `toolExposure` can single out tools (e.g. `"initial_instructions": "direct"`).
|
|
232
|
+
|
|
233
|
+
What changes with `pi.yml` (`single_project: true` + `--project-from-cwd`):
|
|
234
|
+
|
|
235
|
+
- the project auto-activates at startup; `activate_project` and `get_current_config` are dropped from the toolset (the SessionStart `activate` hook still helps — it nudges the agent to read the manual via `initial_instructions`);
|
|
236
|
+
- the six duplicate tools disappear, removing the temptation to use them;
|
|
237
|
+
- the symbol-first rules ride in the system prompt instead of arriving as after-the-fact nudges.
|
|
238
|
+
|
|
239
|
+
Keep the hooks as a fallback: `remind` with `denyAsContext: true` (catches bursts the prompt didn't prevent), `activate`, and `cleanup` all stay useful; `auto-approve` remains inert under pi (see warning above).
|
|
240
|
+
|
|
200
241
|
## Config Override
|
|
201
242
|
|
|
202
243
|
Set `PI_HOOKS_CONFIG` env var to point to a custom config path (exclusive single source; when set, no other location is consulted).
|
package/index.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* least one hook wins; a valid-but-hookless file falls through to the next
|
|
8
8
|
* candidate instead of silently disabling everything below it):
|
|
9
9
|
* 1. PI_HOOKS_CONFIG env (exclusive single source when set)
|
|
10
|
-
* 2. ~/.pi/agent/
|
|
11
|
-
* 3. <cwd>/.pi/
|
|
12
|
-
* 4. ~/.pi/
|
|
10
|
+
* 2. ~/.pi/agent/hook.json (user-global, via getAgentDir())
|
|
11
|
+
* 3. <cwd>/.pi/hook.json (project-local)
|
|
12
|
+
* 4. ~/.pi/hook.json (legacy home location)
|
|
13
13
|
*
|
|
14
14
|
* and maps:
|
|
15
15
|
* SessionStart → session_start (source = mapped reason)
|
|
@@ -167,9 +167,9 @@ export function loadConfig(cwd: string): HooksConfig | null {
|
|
|
167
167
|
const candidates = envPath
|
|
168
168
|
? [envPath]
|
|
169
169
|
: [
|
|
170
|
-
join(getAgentDir(), "
|
|
171
|
-
join(cwd, CONFIG_DIR_NAME, "
|
|
172
|
-
join(homedir(), CONFIG_DIR_NAME, "
|
|
170
|
+
join(getAgentDir(), "hook.json"), // ~/.pi/agent/hook.json — user-global, top priority
|
|
171
|
+
join(cwd, CONFIG_DIR_NAME, "hook.json"), // project-local
|
|
172
|
+
join(homedir(), CONFIG_DIR_NAME, "hook.json"), // legacy home
|
|
173
173
|
];
|
|
174
174
|
|
|
175
175
|
// First valid config that defines hooks wins. A candidate that parses but
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
2
|
+
"name": "@fyeeme/pi-hooks",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Claude Code-compatible hooks runner for pi. Reads hooks config (priority: ~/.pi/agent/hook.json, then project .pi/hook.json) and maps SessionStart, PreToolUse, and Stop events to pi lifecycle events.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "fyeeme",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/fyeeme/pi-packages"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/fyeeme/pi-packages/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/fyeeme/pi-packages#readme",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi",
|
|
25
|
+
"hooks",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"serena",
|
|
28
|
+
"automation",
|
|
29
|
+
"lifecycle"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"index.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./index.ts"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "vitest --run",
|
|
44
|
+
"typecheck": "tsc"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@earendil-works/pi-coding-agent": "0.99.2",
|
|
51
|
+
"@types/node": "22.19.19",
|
|
52
|
+
"typescript": "5.9.3"
|
|
53
|
+
}
|
|
54
54
|
}
|