@rwese/pi-hooks
v0.2.0
Published
Run user-defined hooks on pi events (input, agent_end)
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/erroroutput
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-hooksOr 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.tsHook 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_modulesvianpx tsx - 30-second timeout per hook
- Hooks are skipped for non-interactive input (RPC, extensions)
tool_callandbefore_agent_startcan block execution- Other hooks are informational only (can't block)
- Use
/hooks:listto 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
