# How the PreToolUse Hook Injects Mid-Workflow Updates Using additionalContext

> Learn how the PreToolUse hook injects mid-workflow updates using additionalContext in letta-ai/claude-subconscious. Get real-time agent updates and synchronize state seamlessly.

- Repository: [Letta/claude-subconscious](https://github.com/letta-ai/claude-subconscious)
- Tags: internals
- Published: 2026-03-26

---

**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`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks/hooks.json), which invokes `node scripts/pretool_sync.ts` before every tool invocation. In [`scripts/pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/agent_config.ts), and the conversation ID via `lookupConversation()` (lines 94-98) from [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/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:

```json
{
  "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

- **[`scripts/pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/pretool_sync.ts)** – Contains the core hook logic including `readHookInput()`, `detectChangedBlocks()`, and `formatOutput()`.
- **[`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts)** – Provides persistence utilities `loadSyncState()` and `saveSyncState()`, plus `lookupConversation()` for resolving conversation IDs.
- **[`hooks/hooks.json`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks/hooks.json)** – Registers the PreToolUse event to trigger [`pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/pretool_sync.ts).
- **[`scripts/agent_config.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/agent_config.ts)** – Supplies `getAgentId()` for identifying the active Letta agent.
- **[`scripts/letta_api_url.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/letta_api_url.ts)** – Constructs Letta API endpoints used by the fetch 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:

```typescript
// 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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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.