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 |
PostToolUseFailure | After tool fails (stderr fed back to agent) | 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 |
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, PostToolUseFailure, 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.
Hook types
Section titled “Hook types”Letta Code supports two types of hooks: command hooks that run shell scripts, and prompt hooks that use an LLM to evaluate actions.
Command hooks
Section titled “Command hooks”Command hooks execute shell scripts and use exit codes to allow or block actions. This is the default hook type, covered in detail in the Writing hooks section below.
{ "type": "command", "command": "./hooks/validate-bash.sh", "timeout": 60000}Prompt hooks
Section titled “Prompt hooks”Prompt hooks send the hook input to an LLM for evaluation instead of running a shell script. The LLM decides whether to allow or block the action based on your prompt.
{ "type": "prompt", "prompt": "Block any Bash commands that modify files outside the src/ directory. Only allow read operations on system files.", "model": "haiku", "timeout": 30000}| Field | Required | Description |
|---|---|---|
type | Yes | Must be "prompt" |
prompt | Yes | Instructions for the LLM to evaluate the action |
model | No | Model to use for evaluation (defaults to agent’s model) |
timeout | No | Timeout in milliseconds (default: 30000) |
Use $ARGUMENTS in your prompt to reference the hook input JSON. If omitted, the input is appended automatically.
Prompt hooks are shown with a ✦ indicator in the /hooks manager.
Supported events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, UserPromptSubmit, Stop, SubagentStop
Example: Block dangerous operations with natural language
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "prompt", "prompt": "Block any commands that delete files, modify system configuration, or access sensitive directories like /etc or ~/.ssh. Allow normal development commands.", "model": "haiku" } ] } ] }}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 useruser_message- the user’s original prompt that started this turn
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...", "user_message": "What does this project look like?"}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.
Additional context injection
Section titled “Additional context injection”PostToolUse hooks that exit with code 0 can inject additional context into the agent’s conversation by printing JSON to stdout. The JSON should contain an additionalContext field (either at the top level or nested under hookSpecificOutput):
{ "additionalContext": "The tests passed but code coverage dropped to 72%."}Or:
{ "hookSpecificOutput": { "additionalContext": "Lint warnings found in the changed files." }}The additionalContext string is fed back to the agent as context after the tool completes. Non-JSON stdout is ignored.
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" } ] } ] }}