@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.

Packages

Package details

extension

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):

  1. PI_HOOKS_CONFIG env var (exclusive single source when set)
  2. ~/.pi/agent/hooks.json — user-global, top priority
  3. <project>/.pi/hooks.json — project-local fallback
  4. ~/.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 like plugin_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. terminate skips 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 python3 failing 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-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.

--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).