@rwese/pi-hooks
Run user-defined hooks on pi events (input, agent_end)
Package details
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/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-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_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