Summary
Lifecycle hooks (Stop, UserPromptSubmit, SessionStart, SessionEnd) only fire in the interactive CLI (App.tsx). The headless mode (headless.ts) -- used by SDK subprocess consumers like lettabot -- has zero integration for these hooks.
Tool hooks already work in headless. PreToolUse, PostToolUse, and PostToolUseFailure are wired in tools/manager.ts and cli/helpers/accumulator.ts, which execute regardless of mode. This issue is only about the lifecycle hooks.
Motivation
Community user running 3 lettabot instances on a Raspberry Pi wants to POST each agent exchange to a local analytics server. The Stop hook already provides the right input data (user_message, assistant_message, preceding_reasoning), but it never fires because lettabot's SDK subprocess runs in headless mode.
This blocks any SDK consumer from using lifecycle hooks for analytics, logging, policy enforcement, or external integrations.
Ref: letta-ai/lettabot#494
Current State
| Hook |
Interactive (App.tsx) |
Headless (headless.ts) |
| PreToolUse |
Yes (via tools/manager.ts) |
Yes (same path) |
| PostToolUse |
Yes (via tools/manager.ts + accumulator.ts) |
Yes (same path) |
| PostToolUseFailure |
Yes (via tools/manager.ts) |
Yes (same path) |
| Stop |
Yes (App.tsx:4491) |
No |
| UserPromptSubmit |
Yes (App.tsx:6755) |
No |
| SessionStart |
Yes (App.tsx:1694) |
No |
| SessionEnd |
Yes (App.tsx:1716) |
No |
| Notification |
Yes |
No (may not apply) |
Implementation Notes
All hook infrastructure already exists in src/hooks/. The runner functions (runStopHooks, runSessionStartHooks, etc.) have clean APIs with workingDirectory parameters that default to process.cwd().
Stop hook (highest priority)
Two integration points in headless.ts:
-
Bidirectional mode (runBidirectionalMode): After the approval loop breaks with end_turn (~line 3270), before emitting ResultMessage (~line 3504). The buffers already have assistant message, reasoning, and user message -- same extraction pattern as App.tsx:4474-4488.
-
One-shot mode: After `stopReason === "end_turn"\ (~line 1793), before emitting the result.
Blocking semantics (exit code 2): In bidirectional mode, send hook feedback as a user message and continue the conversation loop (same as App.tsx). One-shot mode already has a turn loop for requires_approval; Stop hook blocking is analogous.
SessionStart / SessionEnd
Fire-and-forget (cannot block). Add after agent resolution / on process exit.
UserPromptSubmit (bidirectional only)
After parsing user input (~line 2969), before sending to agent. If blocked, emit error event and skip.
Scope
~50-80 lines added to headless.ts. Single file change, no new infrastructure.
Working Directory Note
Hooks load from process.cwd() by default. In SDK subprocess context, cwd is whatever the consumer sets. Global hooks (~/.letta/settings.json) work regardless. Project hooks require .letta/settings.json in the subprocess cwd.
Summary
Lifecycle hooks (
Stop,UserPromptSubmit,SessionStart,SessionEnd) only fire in the interactive CLI (App.tsx). The headless mode (headless.ts) -- used by SDK subprocess consumers like lettabot -- has zero integration for these hooks.Tool hooks already work in headless.
PreToolUse,PostToolUse, andPostToolUseFailureare wired intools/manager.tsandcli/helpers/accumulator.ts, which execute regardless of mode. This issue is only about the lifecycle hooks.Motivation
Community user running 3 lettabot instances on a Raspberry Pi wants to POST each agent exchange to a local analytics server. The
Stophook already provides the right input data (user_message,assistant_message,preceding_reasoning), but it never fires because lettabot's SDK subprocess runs in headless mode.This blocks any SDK consumer from using lifecycle hooks for analytics, logging, policy enforcement, or external integrations.
Ref: letta-ai/lettabot#494
Current State
Implementation Notes
All hook infrastructure already exists in
src/hooks/. The runner functions (runStopHooks,runSessionStartHooks, etc.) have clean APIs withworkingDirectoryparameters that default toprocess.cwd().Stop hook (highest priority)
Two integration points in
headless.ts:Bidirectional mode (
runBidirectionalMode): After the approval loop breaks withend_turn(~line 3270), before emittingResultMessage(~line 3504). The buffers already have assistant message, reasoning, and user message -- same extraction pattern asApp.tsx:4474-4488.One-shot mode: After `stopReason === "end_turn"\ (~line 1793), before emitting the result.
Blocking semantics (exit code 2): In bidirectional mode, send hook feedback as a user message and continue the conversation loop (same as App.tsx). One-shot mode already has a turn loop for
requires_approval; Stop hook blocking is analogous.SessionStart / SessionEnd
Fire-and-forget (cannot block). Add after agent resolution / on process exit.
UserPromptSubmit (bidirectional only)
After parsing user input (~line 2969), before sending to agent. If blocked, emit error event and skip.
Scope
~50-80 lines added to
headless.ts. Single file change, no new infrastructure.Working Directory Note
Hooks load from
process.cwd()by default. In SDK subprocess context, cwd is whatever the consumer sets. Global hooks (~/.letta/settings.json) work regardless. Project hooks require.letta/settings.jsonin the subprocess cwd.