Hooks
Run custom scripts at key points in the agent lifecycle
Hooks let you run custom scripts at key points during Letta Code’s execution. Use them to enforce policies, automate workflows, or integrate with external tools.
Hook Lifecycle
Section titled “Hook Lifecycle”Hooks fire at specific points during a Letta Code session:
| Hook | When it fires | Can block? |
|---|---|---|
PreToolUse | Before tool execution | Yes |
PostToolUse | After tool completes successfully | No |
PermissionRequest | When permission dialog appears | Yes |
UserPromptSubmit | User submits a prompt | Yes |
Notification | Letta Code sends a notification | No |
Stop | Agent finishes responding | Yes |
SubagentStop | Subagent task completes | Yes |
PreCompact | Before context compaction | No |
Setup | CLI invoked with --init | No |
SessionStart | Session begins or resumes | No |
SessionEnd | Session terminates | No |
Configuration
Section titled “Configuration”Hooks are configured in your settings files. Letta Code checks three locations (in priority order):
.letta/settings.local.json- Local project settings (not committed).letta/settings.json- Project settings (can commit to share with team)~/.letta/settings.json- User global settings
Hooks from all locations are merged, with local hooks running first.
Structure
Section titled “Structure”Tool-related hooks (PreToolUse, PostToolUse, PermissionRequest) use matchers to specify which tools to target:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "./hooks/validate-bash.sh" } ] } ] }}Simple hooks (Stop, Notification, etc.) don’t need matchers:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "./hooks/run-on-stop.sh" } ] } ] }}Matcher Patterns
Section titled “Matcher Patterns”| Pattern | Matches |
|---|---|
"Bash" | Exact tool name |
"Edit|Write" | Multiple tools (regex alternation) |
"Notebook.*" | Regex pattern |
"*" or "" | All tools |
Managing Hooks
Section titled “Managing Hooks”Use the /hooks command to view and manage your hook configuration interactively.
Writing Hooks
Section titled “Writing Hooks”Input Format
Section titled “Input Format”Hooks receive JSON data via stdin containing context about the event:
{ "event_type": "PreToolUse", "working_directory": "/path/to/project", "tool_name": "Bash", "tool_input": { "command": "npm test" }}The exact fields vary by event type. Tool-related events include tool_name and tool_input. Session events include agent and conversation IDs.
Reasoning and Message Context
Section titled “Reasoning and Message Context”PostToolUse and Stop hooks include the agent’s reasoning and messages that led to the action:
PostToolUse includes:
preceding_reasoning- the agent’s thinking that led to the tool callpreceding_assistant_message- any assistant text before the tool call
Stop includes:
preceding_reasoning- the agent’s thinking that led to the final responseassistant_message- the agent’s final message to the user
Example Stop hook input:
{ "event_type": "Stop", "stop_reason": "end_turn", "preceding_reasoning": "The user asked about the project structure. I should summarize what I found.", "assistant_message": "Here's an overview of the project structure..."}This enables logging, analysis, and automation based on agent reasoning.
Exit Codes
Section titled “Exit Codes”| Exit Code | Behavior |
|---|---|
0 | Success - action proceeds normally |
1 | Non-blocking error - logged, action continues |
2 | Blocking error - action stopped, stderr shown to agent |
For blocking hooks (PreToolUse, PermissionRequest, Stop, etc.), exit code 2 prevents the action and sends your stderr message to the agent as feedback.
Timeouts
Section titled “Timeouts”Hooks have a default timeout of 60 seconds. You can configure a custom timeout per hook:
{ "type": "command", "command": "./hooks/slow-check.sh", "timeout": 120000}Timeout is specified in milliseconds.
Examples
Section titled “Examples”Block dangerous commands
Section titled “Block dangerous commands”Prevent rm -rf commands while still allowing the agent to work autonomously:
#!/bin/bash# Block dangerous rm -rf commands
input=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name')
# Only check Bash commandsif [ "$tool_name" != "Bash" ]; then exit 0fi
command=$(echo "$input" | jq -r '.tool_input.command')
# Check for rm -rf patternif echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)'; then echo "Blocked: rm -rf commands must be run manually." >&2 exit 2fi
exit 0{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "./hooks/block-rm-rf.sh" } ] } ] }}Auto-fix on changes
Section titled “Auto-fix on changes”Run linting automatically after the agent makes changes:
#!/bin/bash# Run bun run fix if there are uncommitted changes
# Check if there are any uncommitted changesif git diff --quiet HEAD 2>/dev/null; then echo "No changes, skipping." exit 0fi
# Run fix - capture output and send to stderr on failureoutput=$(bun run fix 2>&1)exit_code=$?
if [ $exit_code -eq 0 ]; then echo "$output" exit 0else echo "$output" >&2 exit 2fi{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "./hooks/fix-on-changes.sh" } ] } ] }}Type checking on changes
Section titled “Type checking on changes”Similar to linting, run type checking after changes:
#!/bin/bash# Run typecheck if there are uncommitted changes
if git diff --quiet HEAD 2>/dev/null; then echo "No changes, skipping." exit 0fi
output=$(tsc --noEmit --pretty 2>&1)exit_code=$?
if [ $exit_code -eq 0 ]; then echo "$output" exit 0else echo "$output" >&2 exit 2fiLog agent reasoning
Section titled “Log agent reasoning”Capture agent reasoning and responses to a log file for analysis:
#!/bin/bash# Log agent reasoning and responses
input=$(cat)timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")event_type=$(echo "$input" | jq -r '.event_type')reasoning=$(echo "$input" | jq -r '.preceding_reasoning // empty')message=$(echo "$input" | jq -r '.assistant_message // empty')
log_file="${HOME}/.letta/reasoning.log"
if [ -n "$reasoning" ] || [ -n "$message" ]; then echo "=== $timestamp ($event_type) ===" >> "$log_file" [ -n "$reasoning" ] && echo "Reasoning: $reasoning" >> "$log_file" [ -n "$message" ] && echo "Message: $message" >> "$log_file" echo "" >> "$log_file"fi
exit 0{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "./hooks/log-reasoning.sh" } ] } ] }}Desktop notifications
Section titled “Desktop notifications”Get notified when Letta Code needs attention (macOS):
#!/bin/bash# Send desktop notification using osascript (macOS)
input=$(cat)message=$(echo "$input" | jq -r '.message')level=$(echo "$input" | jq -r '.level')
if [ "$level" = "error" ]; then osascript -e "display notification \"$message\" with title \"Letta Code\" subtitle \"Error\""elif [ "$level" = "warning" ]; then osascript -e "display notification \"$message\" with title \"Letta Code\" subtitle \"Warning\""else osascript -e "display notification \"$message\" with title \"Letta Code\""fi
exit 0{ "hooks": { "Notification": [ { "hooks": [ { "type": "command", "command": "./hooks/desktop-notification.sh" } ] } ] }}Best Practices
Section titled “Best Practices”- Make scripts executable - Run
chmod +x hooks/*.shon your hook scripts - Use absolute paths or relative to project root - Commands run from the working directory
- Parse input with jq - The JSON input is well-structured; use
jqfor reliable parsing - Keep hooks fast - Hooks run synchronously and block the agent while executing
- Test hooks manually first - Pipe sample JSON to your script to verify behavior
- Use exit code 2 sparingly - Only block when you have actionable feedback for the agent