@rwese/pi-hooks

Run user-defined hooks on pi events (input, agent_end)

Package details

extension

Install @rwese/pi-hooks from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@rwese/pi-hooks
Package
@rwese/pi-hooks
Version
0.2.0
Published
Apr 22, 2026
Downloads
61/mo · 61/wk
Author
rwese
License
MIT
Types
extension
Size
76.1 KB
Dependencies
0 dependencies · 1 peer
Pi manifest JSON
{
  "extensions": [
    "./extensions"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-hooks

Run user-defined hooks on all pi events.

Features

  • Per-project hooks: Place hooks in .pi/pi-hooks/ in your project
  • Global hooks: Place hooks in ~/.pi/pi-hooks/ for all projects
  • All events: Hook into any pi Extension event
  • Multiple hooks per event: Each hook lives in its own directory
  • User confirmation: On hook failure, presents options to ignore or abort
  • Output persistence: Hook output saved to temp file for reference
  • Commands: List and configure hook settings
  • Execution visibility: Toggle "show hook execution" to see notifications when hooks run
  • Verbose output: See what hooks are doing via console.log/warn/error output

Supported Events

Each hook declares which event it listens to via the event export.

Hook Trigger Can Block? Can Modify? Use Cases
Agent
before_agent_start Before LLM dispatch ✅ Yes ✅ Transform Validate, inject context
agent_start Agent starts ❌ No Logging, setup
agent_start Agent starts ❌ No Logging, setup
agent_end Agent completes ❌ No Post-processing, logging
Turn
turn_start Turn begins ❌ No Per-turn setup
turn_end Turn ends ❌ No Per-turn cleanup
Message
message_start Message starts ❌ No Track message flow
message_update Message updates (streaming) ❌ No Monitor streaming
message_end Message ends ❌ No Track completion
Context
context Before LLM call ❌ No ✅ Transform Filter/inject messages
Tool Execution
tool_execution_start Tool starts ❌ No Track execution
tool_execution_update Tool progress ❌ No Monitor progress
tool_execution_end Tool ends ❌ No Track completion
tool_call Before tool runs ✅ Yes ✅ Transform Block/modify tool arguments
tool_result After tool completes ❌ No ✅ Modify Validate, log, modify output
Session
session_start Session starts ❌ No Setup, clear state
session_shutdown Session ends ❌ No Cleanup
session_before_switch Before session switch ❌ No Warn on dirty state
session_before_fork Before fork ❌ No Prepare fork
session_before_compact Before compaction ❌ No Customize summary
session_compact After compaction ❌ No Post-compact actions
session_before_tree Before tree nav ❌ No Prepare navigation
session_tree After tree nav ❌ No Post-nav actions
Model
model_select Model changes ❌ No Log model changes
Provider
before_provider_request Before API call ❌ No Debug payloads
after_provider_response After API response ❌ No Debug responses
Resource
resources_discover Resources discovered ❌ No Log resources
User Bash
user_bash User runs ! or !! ❌ No Intercept commands

Installation

pi install git:https://github.com/rwese/pi-hooks

Or copy the extension to ~/.pi/agent/extensions/pi-hooks/.

Creating Hooks

Location

Each hook lives in its own directory with an index.ts file that exports its event type.

Project-specific:

your-project/
├── .pi/
│   └── pi-hooks/
│       ├── my_input_validator/
│       │   └── index.ts
│       ├── block_dangerous/
│       │   └── index.ts
│       └── log_changes/
│           └── index.ts
└── ...

Global:

~/.pi/
└── pi-hooks/
    ├── my_input_validator/
    │   └── index.ts
    ├── block_dangerous/
    │   └── index.ts
    └── log_changes/
        └── index.ts

Hook Format

// ~/.pi/pi-hooks/log_changes/index.ts

// Declare which event this hook listens to
export const event = "tool_result" as const;

interface ToolResultPayload {
  event: "tool_result";
  toolCallId: string;
  toolName: string;       // "edit", "write", "bash", etc.
  input: object;           // Tool arguments
  content: Content[];     // Tool output
  details: object;        // Tool-specific details
  isError: boolean;
}

export default function logChangesHook(
  payload: ToolResultPayload
): boolean | void {
  const { toolName, input } = payload;

  // Log file modifications
  if (toolName === "edit" || toolName === "write") {
    const path = input.path as string;
    console.log(`[HOOK] ${toolName}: ${path}`);
  }

  return true; // Pass
  // return false; // Fail
}

before_agent_start Hook (Can Block & Transform)

// ~/.pi/pi-hooks/pre_dispatch/index.ts

export const event = "before_agent_start" as const;

interface BeforeAgentStartPayload {
  event: "before_agent_start";
  prompt: string;
  images: Image[];
  systemPrompt: string;
}

// Return false to block, or modified payload to transform
export default function preDispatchHook(
  payload: BeforeAgentStartPayload
): boolean | void | { prompt?: string; images?: Image[]; systemPrompt?: string } {
  const { prompt } = payload;

  // Auto-correct shorthand commands
  if (prompt.startsWith("test ")) {
    return {
      prompt: prompt.replace(/^test /, "test(unit): "),
    };
  }

  return true; // Pass unchanged
}

tool_call Hook (Can Block & Modify)

// ~/.pi/pi-hooks/block_dangerous/index.ts

export const event = "tool_call" as const;

interface ToolCallPayload {
  event: "tool_call";
  toolCallId: string;
  toolName: string;
  input: object;
}

// Return modified input to transform tool arguments
export default function blockDangerous(
  payload: ToolCallPayload
): boolean | void | { input: Record<string, unknown> } {
  const { toolName, input } = payload;

  // Block dangerous bash commands
  if (toolName === "bash") {
    const command = (input.command as string) || "";
    if (command.includes("rm -rf /")) {
      console.error("Blocking dangerous command!");
      return false; // Block
    }
  }

  // Modify tool arguments by returning new input
  if (toolName === "bash") {
    const command = input.command as string;
    if (!command.includes("--no-preserve-root")) {
      // Add safety flag
      return {
        passed: true,
        modified: true,
        input: {
          ...input,
          command: command + " --no-preserve-root",
        },
      };
    }
  }

  return true;
}

context Hook (Can Transform Messages)

// ~/.pi/pi-hooks/context_transformer/index.ts

export const event = "context" as const;

interface Message {
  role: "user" | "assistant" | "system";
  content: unknown;
}

interface ContextPayload {
  event: "context";
  messages: Message[];
}

export default function contextTransformerHook(
  payload: ContextPayload
): boolean | void | { messages: Message[] } {
  const { messages } = payload;

  // Example: Filter to last N messages
  const MAX = 20;
  if (messages.length > MAX) {
    return {
      passed: true,
      modified: true,
      messages: messages.slice(-MAX),
    };
  }

  return true; // Pass unchanged
}

tool_result Hook (Can Modify Output)

// ~/.pi/pi-hooks/validate_git/index.ts

export const event = "tool_result" as const;

interface ToolResultPayload {
  event: "tool_result";
  toolCallId: string;
  toolName: string;
  input: object;
  content: Content[];
  details: object;
  isError: boolean;
}

export default function validateGit(
  payload: ToolResultPayload
): boolean | void {
  const { toolName, content } = payload;

  // Validate git operations
  if (toolName === "bash") {
    const output = content
      .filter(c => c.type === "text")
      .map(c => c.text)
      .join("");

    if (output.includes("CONFLICT")) {
      console.error("[HOOK] Git conflict detected!");
    }
  }

  return true;
}

Example Hooks

See the examples/ directory for complete implementations:

Example Event Description
before_agent_start/ before_agent_start Validates/modifies before LLM dispatch

| block_dangerous_commands/ | tool_call | Blocks dangerous bash commands | | log_file_modifications/ | tool_result | Logs all file modifications | | trim_tool_whitespace/ | tool_call | Trims trailing whitespace from input | | dirty_edit_guard/ | tool_call | Prevents edits to modified files | | context_transformer/ | context | Filters/transforms messages before LLM call | | agent_end/ | agent_end | Logs after agent completes |

Commands

Command Description
/hooks:list List available hooks (shows name and event type)
/hooks:show [on|off] Toggle hook execution visibility
/hooks:verbose [on|off] Toggle verbose hook output
/hooks:dirty-edit-clear Clear tracked file states
/hooks:disable <event> Disable an event type for this session
/hooks:enable <event> Re-enable a disabled event type

Hook Return Values

Return Meaning
true or undefined Hook passed
false Hook failed (blocked for blocking events)
throw new Error(msg) Hook failed with message
{ prompt?, images?, systemPrompt? } For before_agent_start: return transformed payload
{ input: {...} } For tool_call: return transformed tool arguments
{ messages: [...] } For context: return transformed messages
{ content, details, isError } For tool_result: return modified result

Notes

  • Hooks run with access to node_modules via npx tsx
  • 30-second timeout per hook
  • Hooks are skipped for non-interactive input (RPC, extensions)
  • tool_call and before_agent_start can block execution
  • Other hooks are informational only (can't block)
  • Use /hooks:list to see which hooks are active
  • Multiple hooks can listen to the same event type
  • Project hooks take precedence over global hooks with the same name