@fyeeme/pi-hooks
Claude Code-compatible hooks runner for pi. Reads .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.2- Published
- Aug 8, 2026
- Downloads
- 65/mo · 18/wk
- Author
- fyeeme
- License
- MIT
- Types
- extension
- Size
- 30.7 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 .pi/hooks.json from your project and maps SessionStart, PreToolUse, and Stop events to pi lifecycle events — matching Claude Code's hooks protocol including stdin JSON and stdout additionalContext capture.
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/hooks.json in your project root:
{
"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) → 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 (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.
Config Override
Set PI_HOOKS_CONFIG env var to point to a custom config path.