How the PreToolUse Hook Injects Mid-Workflow Updates Using additionalContext

The PreToolUse hook captures real-time Letta agent updates by reading session metadata from stdin, synchronizing state against persisted snapshots, detecting memory block changes and new assistant messages, then emitting a JSON payload with additionalContext that Claude prepends to the immediate next prompt.

The letta-ai/claude-subconscious repository enables Claude Desktop to maintain continuity with a persistent Letta agent through bidirectional synchronization. The PreToolUse hook serves as the critical injection point for mid-workflow communication, surfacing fresh memory updates via the additionalContext field immediately before any tool execution.

How PreToolUse Captures Real-Time Agent State

Hook Entry Point and Input Reading

The hook activates via the PreToolUse entry in hooks/hooks.json, which invokes node scripts/pretool_sync.ts before every tool invocation. In scripts/pretool_sync.ts, the main() function first checks for LETTA_MODE=off and exits silently if disabled (lines 59-62). The readHookInput() function (lines 72-96) parses a JSON blob from stdin containing session_id, cwd, and tool_name, implementing a 100ms timeout to prevent process hanging.

State Synchronization and Conversation Resolution

The hook loads the previous synchronization snapshot using loadSyncState() from scripts/conversation_utils.ts (lines 81-88). If the state file has never been created—indicated by the condition !state.lastBlockValues && !state.lastSeenMessageId—the hook aborts silently, deferring initial synchronization to the UserPromptSubmit hook.

After resolving the active agent via getAgentId() (line 90) in scripts/agent_config.ts, and the conversation ID via lookupConversation() (lines 94-98) from scripts/conversation_utils.ts, the script executes parallel fetches to minimize latency.

Detecting Changes in Memory and Messages

The fetchAgent() function (lines 100-119) retrieves the current agent object including all memory blocks, while fetchNewMessages() (lines 124-180) queries the Letta API for assistant messages newer than the stored lastSeenMessageId. The detectChangedBlocks() function (lines 185-197) performs a structural comparison between freshly fetched memory blocks and the cached lastBlockValues, identifying added, modified, or removed entries.

Formatting and Injecting additionalContext

Building the XML Context Payload

The formatOutput() function (lines 200-252) constructs an XML-structured string containing:

  • <letta_message> elements for each new assistant communication, attributed with timestamps and sender identity.
  • <letta_memory_update> sections with <label> blocks showing line-level diffs for changed memory blocks.

This formatted content is wrapped in a root <letta_update> envelope and combined with an <instruction> element reminding Claude to briefly acknowledge the Subconscious note in its next response.

The Final JSON Output Structure

The hook assembles the final payload in lines 336-452, printing to stdout a strictly formatted JSON object:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "<letta_update>\n<letta_message from=\"Subconscious\" timestamp=\"2024-05-01T12:34Z\">\nHere’s a quick tip …\n</letta_message>\n\n<letta_memory_update>\n<Goals status=\"modified\">\n- old goal line\n+ new goal line\n</Goals>\n</letta_memory_update>\n</letta_update>\n\n<instruction>Your Subconscious (Subconscious) just sent a message mid‑workflow. Briefly acknowledge what Subconscious said in your next response – just a short note like \"Sub notes: …\" so the user knows.</instruction>"
  }
}

Claude's runtime extracts hookSpecificOutput.additionalContext and prepends it to the next prompt, ensuring the model incorporates the latest Letta state before executing the tool or responding to the user.

Key Implementation Files and Functions

Code Example: Simulating a Pre-Tool-Use Invocation

The following TypeScript snippet demonstrates how to simulate the stdin input that Claude Desktop provides when triggering the hook:

// Simulated stdin for the hook (normally supplied by Claude)
const hookInput = {
  session_id: "abc123",
  cwd: "/my/project",
  hook_event_name: "PreToolUse",
  tool_name: "search_web"
};
process.stdin.push(JSON.stringify(hookInput));
process.stdin.end();

// Run the hook: node scripts/pretool_sync.ts
// The hook prints JSON to stdout containing additionalContext built from
// any newly-received Letta messages or changed memory blocks.

Summary

  • The PreToolUse hook executes immediately before any Claude tool invocation, registered via hooks/hooks.json.
  • It reads session metadata from stdin using readHookInput() with a 100ms safety timeout.
  • State persistence relies on loadSyncState() and saveSyncState() in scripts/conversation_utils.ts to track lastSeenMessageId and lastBlockValues.
  • Parallel API calls in fetchAgent() and fetchNewMessages() minimize latency while detectChangedBlocks() identifies deltas.
  • The hook outputs a JSON payload to stdout containing hookSpecificOutput.additionalContext, which Claude's runtime prepends to the next prompt.
  • Failures are intentionally non-blocking; the hook exits with code 0 even when errors occur (logging only when LETTA_DEBUG=1).

Frequently Asked Questions

What triggers the PreToolUse hook in Claude Desktop?

The hook triggers immediately before Claude executes any tool call, as configured in hooks/hooks.json which maps the PreToolUse event to node scripts/pretool_sync.ts. This ensures the LLM receives the latest Letta agent state before acting.

How does the hook distinguish new messages from previously seen ones?

The hook maintains a lastSeenMessageId in the persisted sync state managed by scripts/conversation_utils.ts. The fetchNewMessages() function queries the Letta API for messages newer than this stored ID, ensuring only unseen assistant communications are included in the additionalContext payload.

What happens if the Letta API is unavailable when the hook runs?

The hook implements a try-catch block (lines 554-557) that catches all exceptions and exits with code 0, making failures non-blocking. If LETTA_DEBUG=1 is set, the error details are logged to stderr, but Claude continues with the tool execution using existing context.

Can the additionalContext injection be disabled without uninstalling the hook?

Yes. Setting the environment variable LETTA_MODE=off causes the main() function in scripts/pretool_sync.ts (lines 59-62) to return early without emitting any additionalContext, effectively disabling mid-workflow updates while keeping the hook registered for future use.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →