Skip to content
Letta Code Letta Code Letta Docs
Sign up
Features

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.

Hooks fire at specific points during a Letta Code session:

HookWhen it firesCan block?
PreToolUseBefore tool executionYes
PostToolUseAfter tool completes successfullyNo
PermissionRequestWhen permission dialog appearsYes
UserPromptSubmitUser submits a promptYes
NotificationLetta Code sends a notificationNo
StopAgent finishes respondingYes
SubagentStopSubagent task completesYes
PreCompactBefore context compactionNo
SetupCLI invoked with --initNo
SessionStartSession begins or resumesNo
SessionEndSession terminatesNo

Hooks are configured in your settings files. Letta Code checks three locations (in priority order):

  1. .letta/settings.local.json - Local project settings (not committed)
  2. .letta/settings.json - Project settings (can commit to share with team)
  3. ~/.letta/settings.json - User global settings

Hooks from all locations are merged, with local hooks running first.

Tool-related hooks (PreToolUse, PostToolUse, PermissionRequest) use matchers to specify which tools to target:

.letta/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./hooks/validate-bash.sh"
}
]
}
]
}
}

Simple hooks (Stop, Notification, etc.) don’t need matchers:

.letta/settings.json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/run-on-stop.sh"
}
]
}
]
}
}
PatternMatches
"Bash"Exact tool name
"Edit|Write"Multiple tools (regex alternation)
"Notebook.*"Regex pattern
"*" or ""All tools

Use the /hooks command to view and manage your hook configuration interactively.

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.

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 call
  • preceding_assistant_message - any assistant text before the tool call

Stop includes:

  • preceding_reasoning - the agent’s thinking that led to the final response
  • assistant_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 CodeBehavior
0Success - action proceeds normally
1Non-blocking error - logged, action continues
2Blocking 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.

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.

Prevent rm -rf commands while still allowing the agent to work autonomously:

hooks/block-rm-rf.sh
#!/bin/bash
# Block dangerous rm -rf commands
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
# Only check Bash commands
if [ "$tool_name" != "Bash" ]; then
exit 0
fi
command=$(echo "$input" | jq -r '.tool_input.command')
# Check for rm -rf pattern
if 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 2
fi
exit 0
.letta/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./hooks/block-rm-rf.sh"
}
]
}
]
}
}

Run linting automatically after the agent makes changes:

hooks/fix-on-changes.sh
#!/bin/bash
# Run bun run fix if there are uncommitted changes
# Check if there are any uncommitted changes
if git diff --quiet HEAD 2>/dev/null; then
echo "No changes, skipping."
exit 0
fi
# Run fix - capture output and send to stderr on failure
output=$(bun run fix 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "$output"
exit 0
else
echo "$output" >&2
exit 2
fi
.letta/settings.json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/fix-on-changes.sh"
}
]
}
]
}
}

Similar to linting, run type checking after changes:

hooks/typecheck-on-changes.sh
#!/bin/bash
# Run typecheck if there are uncommitted changes
if git diff --quiet HEAD 2>/dev/null; then
echo "No changes, skipping."
exit 0
fi
output=$(tsc --noEmit --pretty 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "$output"
exit 0
else
echo "$output" >&2
exit 2
fi

Capture agent reasoning and responses to a log file for analysis:

hooks/log-reasoning.sh
#!/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
.letta/settings.json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/log-reasoning.sh"
}
]
}
]
}
}

Get notified when Letta Code needs attention (macOS):

hooks/desktop-notification.sh
#!/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
.letta/settings.json
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/desktop-notification.sh"
}
]
}
]
}
}
  1. Make scripts executable - Run chmod +x hooks/*.sh on your hook scripts
  2. Use absolute paths or relative to project root - Commands run from the working directory
  3. Parse input with jq - The JSON input is well-structured; use jq for reliable parsing
  4. Keep hooks fast - Hooks run synchronously and block the agent while executing
  5. Test hooks manually first - Pipe sample JSON to your script to verify behavior
  6. Use exit code 2 sparingly - Only block when you have actionable feedback for the agent