# How Claude Subconscious Integrates with Claude Code Hooks: SessionStart, PreToolUse, UserPromptSubmit, and Stop

> Discover how Claude Subconscious integrates with Claude Code hooks like SessionStart, PreToolUse, UserPromptSubmit, and Stop. Learn to synchronize Claude Code sessions and Letta agent conversations via TypeScript scripts.

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

---

**Claude Subconscious integrates with Claude Code hooks by registering TypeScript scripts in [`hooks/hooks.json`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks/hooks.json) that map SessionStart, PreToolUse, UserPromptSubmit, and Stop events to specific handlers, enabling bidirectional synchronization between Claude Code sessions and Letta agent conversations.**

The `letta-ai/claude-subconscious` repository implements a Claude Code plugin that creates a persistent bridge between Claude Code and Letta AI agents. By leveraging Claude Code's hook system, the plugin maintains stateful memory synchronization across the entire development session lifecycle.

## Hook Registration in [`hooks.json`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks.json)

Claude Code discovers plugins through a declarative configuration file. In [`hooks/hooks.json`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks/hooks.json), each event maps to a command executed via the `silent-npx.cjs` wrapper, which suppresses console windows on Windows systems.

The four hook events are registered as follows:

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/silent-npx.cjs\" tsx \"${CLAUDE_PLUGIN_ROOT}/scripts/session_start.ts\"",
            "timeout": 5
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/silent-npx.cjs\" tsx \"${CLAUDE_PLUGIN_ROOT}/scripts/pretool_sync.ts\"",
            "timeout": 5
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/silent-npx.cjs\" tsx \"${CLAUDE_PLUGIN_ROOT}/scripts/sync_letta_memory.ts\"",
            "timeout": 10
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/silent-npx.cjs\" tsx \"${CLAUDE_PLUGIN_ROOT}/scripts/send_messages_to_letta.ts\"",
            "timeout": 120,
            "async": true
          }
        ]
      }
    ]
  }
}

```

Each hook receives a JSON payload via `stdin` containing metadata like `session_id`, `cwd`, and `hook_event_name`.

## SessionStart: Initializing Letta Conversations

When Claude Subconscious integrates with the **SessionStart** hook, it establishes a new Letta conversation for the development session. The handler in [`scripts/session_start.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/session_start.ts) performs several initialization steps:

1. Reads the hook input to extract `session_id` and `cwd`
2. Calls `conversation_utils.getOrCreateConversation` to provision a Letta conversation
3. Persists the mapping in `<cwd>/.letta/claude/conversations.json`
4. Sends an introductory `<claude_code_session_start>` XML message via `sendSessionStartMessage`

The script generates structured XML that includes SDK-tools mode, project name, and session metadata:

```xml
<claude_code_session_start>
  <project>my-project</project>
  <path>/home/user/my-project</path>
  <session_id>c8f9e3b2-1a4d-4b5e-9d6f-1234567890ab</session_id>
  <timestamp>2026-03-26T14:02:33.123Z</timestamp>
  <sdk_tools_mode>read-only</sdk_tools_mode>
  <context>
    A new Claude Code session has begun. I'll be sending you updates as the session progresses.
    Tool access: Read-only tool access — you can Read, Grep, Glob files and search the web.
  </context>
</claude_code_session_start>

```

This initialization ensures every Claude Code session has a corresponding Letta agent conversation with durable state stored in the `.letta/claude/` directory.

## PreToolUse: Injecting Context Before Tool Execution

The **PreToolUse** hook in [`scripts/pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/pretool_sync.ts) intercepts tool calls to surface Letta agent guidance within Claude Code's context. Before Claude executes any tool, this handler:

- Loads the per-session sync state via `loadSyncState`
- Fetches new assistant messages from Letta using `fetchNewMessages`
- Detects memory block changes with `detectChangedBlocks`
- Returns a JSON object with an `additionalContext` field

Claude Code automatically merges this `additionalContext` into the next tool-use prompt. The JSON output structure follows this pattern:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "<letta_message from=\"Subconscious\" timestamp=\"2026-03-26T14:05:10Z\">\nRemember to run `npm test` after the refactor.\n</letta_message>\n\n<letta_memory_update>\n<review_status status=\"modified\">\n- TODO: add unit tests\n+ TODO: add integration tests\n</review_status>\n</letta_memory_update>\n\n<instruction>Your Subconscious (Subconscious) just sent a message above. Briefly acknowledge what Subconscious said in your next response - just a short note like \"Sub notes: …\" so the user knows.</instruction>"
  }
}

```

This mechanism allows the Letta agent to inject real-time guidance, memory updates, and contextual reminders directly into Claude Code's tool execution flow.

## UserPromptSubmit: Merging Memory into Prompts

The **UserPromptSubmit** hook ([`scripts/sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/sync_letta_memory.ts)) activates when the user finishes typing a prompt, enabling Claude Subconscious to integrate Letta memory updates before Claude processes the input. This handler supports two modes:

**Whisper mode** (default): Emits only new assistant messages to stdout
**Full mode**: Also updates [`.claude/CLAUDE.md`](https://github.com/letta-ai/claude-subconscious/blob/main/.claude/CLAUDE.md) with complete memory blocks

The script executes these operations:

1. Loads sync state and retrieves the Letta conversation ID
2. Fetches latest agent data via `fetchAgent`
3. Retrieves new messages using `fetchAssistantMessages`
4. Detects changed blocks and formats diff-style XML via `formatChangedBlocksForStdout`
5. Emits XML to stdout, which Claude Code captures and prefixes to the user prompt

In whisper mode, the stdout output appears as:

```xml
<letta_message from="Subconscious" timestamp="2026-03-26T14:07:45Z">
You might want to rename `fooBar` to `barFoo` for consistency.
</letta_message>

<instruction>Your Subconscious (Subconscious) sent you a message above. Briefly acknowledge what Subconscious said - just a short note like "Sub notes: …" so the user knows.</instruction>

```

In full mode, the script additionally calls `updateClaudeMd` to persist memory blocks to the project's [`.claude/CLAUDE.md`](https://github.com/letta-ai/claude-subconscious/blob/main/.claude/CLAUDE.md) file, ensuring persistent context across sessions.

## Stop: Persisting Transcripts to Letta

When the session ends, the **Stop** hook ([`scripts/send_messages_to_letta.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/send_messages_to_letta.ts)) ensures the complete conversation history reaches the Letta agent. This handler:

1. Reads the transcript JSONL file generated by Claude Code
2. Formats entries as `<message role="...">...</message>` XML within a `<claude_code_session_update>` envelope
3. Spawns a detached background worker via `spawnSilentWorker` running [`scripts/send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/send_worker_sdk.ts)
4. Updates the per-session sync state to track the last processed transcript index

The background worker ([`send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_worker_sdk.ts)) loads the Letta Code SDK (`@letta-ai/letta-code-sdk`) and resumes the conversation:

```typescript
// Executed in send_worker_sdk.ts
session.resumeSession(...).send(payload.message)

```

This asynchronous processing allows the Stop hook to return immediately while the worker streams the assistant's response and handles large transcript transfers without blocking Claude Code's shutdown.

## State Management and API Utilities

Claude Subconscious relies on durable state persistence in `<cwd>/.letta/claude/`:

- [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json): Maps `session_id` to `{conversationId, agentId}`
- `session-<id>.json`: Stores `lastProcessedIndex`, `lastSeenMessageId`, and memory block snapshots

The [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts) module provides shared utilities including `createConversation`, `lookupConversation`, `loadSyncState`, `saveSyncState`, and XML escaping functions. URL construction is handled by [`scripts/letta_api_url.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/letta_api_url.ts), which exports `buildLettaApiUrl` and normalizes the `LETTA_API_BASE` environment variable.

## Summary

- **Claude Subconscious integrates with Claude Code hooks** through declarative registration in [`hooks/hooks.json`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks/hooks.json), mapping SessionStart, PreToolUse, UserPromptSubmit, and Stop events to specific TypeScript handlers.
- **SessionStart** initializes Letta conversations and persists session mappings in [`.letta/claude/conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/.letta/claude/conversations.json).
- **PreToolUse** returns JSON with `additionalContext` containing XML-formatted Letta messages and memory updates that Claude Code injects into tool prompts.
- **UserPromptSubmit** writes XML to stdout (whisper mode) or updates [`CLAUDE.md`](https://github.com/letta-ai/claude-subconscious/blob/main/CLAUDE.md) (full mode) to merge Letta guidance before Claude processes user input.
- **Stop** spawns a background SDK worker to send the complete transcript to Letta without blocking session termination.
- All handlers rely on [`conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/conversation_utils.ts) for state persistence and [`letta_api_url.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/letta_api_url.ts) for API endpoint construction.

## Frequently Asked Questions

### How does Claude Code know which scripts to run for each hook event?

Claude Code reads the [`hooks/hooks.json`](https://github.com/letta-ai/claude-subconscious/blob/main/hooks/hooks.json) file in the plugin root directory, which declares event matchers and associated commands. Each entry specifies the script path, timeout, and whether execution should be asynchronous. Claude Subconscious uses the `silent-npx.cjs` wrapper to execute TypeScript files via `tsx` without spawning visible console windows.

### What is the difference between PreToolUse and UserPromptSubmit hooks?

**PreToolUse** runs before Claude executes any tool, returning JSON that Claude Code merges as `additionalContext` into the tool-use prompt. **UserPromptSubmit** runs after the user types a message but before Claude processes it, writing XML directly to stdout that Claude Code prefixes to the user's prompt. PreToolUse is tool-centric, while UserPromptSubmit is prompt-centric.

### Where does Claude Subconscious store session state between hook invocations?

The plugin stores durable state in a `.letta/claude/` directory within the current working directory. The [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) file maintains the mapping between Claude Code session IDs and Letta conversation IDs, while individual `session-<id>.json` files track synchronization state including the last processed message index and memory block snapshots.

### Why does the Stop hook use a background worker process?

The **Stop** hook spawns [`send_worker_sdk.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/send_worker_sdk.ts) as a detached background worker via `spawnSilentWorker` because transmitting the full conversation transcript to Letta may take significant time. Running asynchronously prevents the hook from blocking Claude Code's session termination while ensuring the Letta agent receives the complete message history for long-term memory consolidation.