@fyeeme/pi-hooks
Claude Code-compatible hooks runner for pi. Reads hooks config (priority: ~/.pi/agent/hooks.json, then project .pi/hooks.json) and maps SessionStart, PreToolUse, and Stop events to pi lifecycle events.
Package details
Install @fyeeme/pi-hooks from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@fyeeme/pi-hooks- Package
@fyeeme/pi-hooks- Version
1.0.5- Published
- Sep 16, 2026
- Downloads
- 239/mo · 166/wk
- Author
- fyeeme
- License
- MIT
- Types
- extension
- Size
- 40.1 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-hooks
A Claude Code-compatible hooks runner for pi. Reads your hooks configuration and maps SessionStart, PreToolUse, and Stop events to pi lifecycle events — matching Claude Code's hooks protocol including stdin JSON and stdout additionalContext capture.
Config resolution order (first file that defines at least one hook wins):
PI_HOOKS_CONFIGenv var (exclusive single source when set)~/.pi/agent/hooks.json— user-global, top priority<project>/.pi/hooks.json— project-local fallback~/.pi/hooks.json— legacy home location fallback
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.
Install
Requires the pi CLI.
From npm (recommended)
# Global (user) install — available in every project
pi install npm:@fyeeme/pi-hooks
# Project-local — written to .pi/settings.json, shareable with your team
pi install -l npm:@fyeeme/pi-hooks
# Pinned version — skipped by `pi update`
pi install npm:@fyeeme/pi-hooks@1.0.0
# Try it once without saving (current run only)
pi -e npm:@fyeeme/pi-hooks
From GitHub
Source: fyeeme/pi-packages.
# HTTPS shorthand
pi install git:github.com/fyeeme/pi-packages
# Pin to a tag or commit (skipped by `pi update`)
pi install git:github.com/fyeeme/pi-packages@v1.0.0
# Raw URL form
pi install https://github.com/fyeeme/pi-packages
See the Pi Packages guide on pi.dev for the full list of source types, scopes, and pi update behavior.
Configuration
Create ~/.pi/agent/hooks.json (user-global, highest file priority — runs in every project) or .pi/hooks.json in a project root (used only when no global config defines hooks):
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "serena-hooks activate --client=claude-code"
}
]
}
],
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "serena-hooks remind --client=claude-code"
}
]
},
{
"matcher": "plugin_serena_serena_.*",
"hooks": [
{
"type": "command",
"command": "serena-hooks auto-approve --client=claude-code"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "serena-hooks cleanup --client=claude-code"
}
]
}
]
}
}
Event Mapping
| hooks.json event | pi event | Notes |
|---|---|---|
SessionStart |
session_start |
Runs on session start/reload/switch. matcher matches the source (startup/resume/clear/...); empty matcher matches all. additionalContext is injected into the first user message via the context event. |
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). |
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. |
Matcher semantics
matcher is a regex (Claude Code compatible), tested against the full tool name (PreToolUse) or session source (SessionStart):
""or"*"— match all"Edit|Write"— match either"Notebook.*"— prefix match"plugin_serena_serena_.*"— all serena tools
Breaking change from 1.0.x: matchers were previously interpreted as globs (
*/?). If you upgraded, convert patterns likeplugin_serena_serena_*→plugin_serena_serena_.*. Invalid regex matches nothing and warns once at first use (never throws).
Protocol
Commands receive Claude Code-compatible JSON on stdin (session_id is the pi session UUID; transcript_path is the conversation JSONL path):
{ "hook_event_name": "SessionStart", "session_id": "<uuid>", "transcript_path": "/path/to/session.jsonl", "cwd": "/proj", "permission_mode": "default", "source": "startup" }
{ "hook_event_name": "PreToolUse", "session_id": "<uuid>", "transcript_path": "...", "cwd": "/proj", "permission_mode": "default", "tool_name": "bash", "tool_input": {} }
{ "hook_event_name": "Stop", "session_id": "<uuid>", "transcript_path": "...", "cwd": "/proj", "permission_mode": "default" }
Commands may return JSON on stdout, or control flow via exit codes:
{ "hookSpecificOutput": { "additionalContext": "context injected into the conversation" } }
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "blocked" } }
- exit code 0 with
additionalContext→ context injected. - exit code 2 (PreToolUse) with a JSON deny payload (
permissionDecision: "deny") → tool call blocked (terminate: true); reason fed to the model.terminateskips the follow-up LLM call only when the denied call is in an all-terminating batch (pi >= 0.84.1, #7715); in a multi-tool batch the block always applies but the agent may continue. - exit code 2 without parseable JSON (e.g. a broken command like
python3failing to open a script) → treated as a crash, not a deny: warning on stderr, tool call proceeds. This keeps a misconfigured hook from hard-blocking every tool call. - exit code 2 (Stop) → ignored (pi cannot block exit).
- other non-zero → logged, execution continues.
- non-JSON stdout → logged as a warning, ignored.
- each hook may set
"timeout"(seconds, default 60); matching hooks run in parallel.
The additionalContext is injected into the pi conversation (appended to the last user message, never as a new turn).
MCP Tool Names
Pi names MCP tools as <serverName>_<toolName> (not mcp__server__tool like Claude Code), so target them with regex like plugin_serena_serena_.*. Check your actual tool names with /mcp in pi to set the correct matcher.
Using pi-hooks with Serena
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/hooks.json (global) or the project's .pi/hooks.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "serena-hooks activate --client=claude-code" }]
}
],
"PreToolUse": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "serena-hooks remind --client=claude-code" }]
},
{
"matcher": "serena_.*",
"hooks": [{ "type": "command", "command": "serena-hooks auto-approve --client=claude-code" }]
}
],
"Stop": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "serena-hooks cleanup --client=claude-code" }]
}
]
}
}
What each hook does:
| Event | Command | Role |
|---|---|---|
SessionStart |
activate |
Prompts the agent to activate the project and read Serena's instructions at session start. |
PreToolUse ("") |
remind |
Nudges the agent to prefer Serena's symbolic tools over raw read/grep. Runs before every tool call. |
PreToolUse (serena_.*) |
auto-approve |
Auto-approves Serena tool calls while the client is in a permissive permission mode. |
Stop |
cleanup |
Clears per-session hook state on exit. |
Get the matcher prefix right. pi exposes an MCP server's tools as <server>_<tool>. With Serena registered as the serena MCP server (the default), tools are named serena_find_symbol, serena_read_file, … → use serena_.*. If you installed Serena as a pi plugin instead, the names are plugin_serena_serena_* → use plugin_serena_serena_.*. Run /mcp in pi to confirm your exact prefix.
⚠️
auto-approveis currently inert under pi-hooks.serena-hooks auto-approveonly emits its approval when stdin reports a permissivepermission_mode(acceptEditsorauto), but pi-hooks always sendspermission_mode: "default"today. The hook still runs but stays silent, so pi's own permission flow applies.activate,remind, andcleanupare unaffected. This will resolve once pi-hooks forwards the real permission mode.
--client=claude-code is correct for pi: pi-hooks speaks the Claude Code hooks protocol, so Serena treats pi as a Claude Code client.
Config Override
Set PI_HOOKS_CONFIG env var to point to a custom config path (exclusive single source; when set, no other location is consulted).