How Claude Subconscious Achieves Two-Way Communication with Claude Code
Claude Subconscious enables two-way communication with Claude Code through three cooperating hook scripts that create a persistent Letta conversation, stream transcript updates via XML messages, and asynchronously inject agent responses back into the Claude Code context.
Claude Subconscious is a Letta agent designed to observe Claude Code sessions and maintain persistent memory across interactions. The letta-ai/claude-subconscious repository implements this two-way communication with Claude Code using TypeScript hook scripts that run inside Claude Code, allowing the Subconscious to both receive session updates and reply to the user asynchronously.
The Three-Hook Architecture
The bidirectional channel relies on three distinct hook scripts that coordinate the flow of information between Claude Code and the Letta agent.
SessionStart Hook: Establishing the Back-Channel
The scripts/session_start.ts script initiates the connection when a new Claude Code session begins. It creates or reuses a Letta conversation, stores the mapping in a durable conversations.json file, and sends a special <claude_code_session_start> XML message to the Letta agent.
According to the source code in session_start.ts (lines 67-78), the script first creates a durable directory for the mapping. Then, as shown in lines 166-176, it constructs and sends the XML payload containing the project name, current working directory, session ID, and SDK tools mode:
// scripts/session_start.ts – create or reuse a Letta conversation
const conversationId = await createConversation(apiKey, agentId, log);
// …
const message = `<claude_code_session_start>
<project>${projectName}</project>
<path>${cwd}</path>
<session_id>${sessionId}</session_id>
<timestamp>${timestamp}</timestamp>
<sdk_tools_mode>${sdkToolsMode}</sdk_tools_mode>
...
</claude_code_session_start>`;
await fetch(buildLettaApiUrl(`/conversations/${conversationId}/messages`), …);
The conversation ID is persisted using conversationUtils.saveConversationsMap to conversations.json, enabling subsequent hooks to locate the same Letta conversation.
Stop Hook: Pushing Updates to Letta
The scripts/send_messages_to_letta.ts script runs at the end of each Claude Code prompt to transmit the session transcript to the Subconscious. It reads the transcript using readTranscript, formats new messages as <claude_code_session_update> XML, and delegates the actual sending to a background worker.
Lines 48-78 handle the transcript reading, while lines 84-95 format the new entries. Lines 128-131 launch the detached worker:
// scripts/send_messages_to_letta.ts – read transcript, format, and spawn worker
const messages = await readTranscript(hookInput.transcript_path, log);
const newMessages = formatMessagesForLetta(messages, state.lastProcessedIndex, log);
const payload = {
agentId,
conversationId,
sessionId: hookInput.session_id,
message: userMessage, // XML <claude_code_session_update>
stateFile,
newLastProcessedIndex: messages.length - 1,
cwd: hookInput.cwd,
sdkToolsMode,
};
fs.writeFileSync(payloadFile, JSON.stringify(payload));
spawnSilentWorker(path.join(__dirname, 'send_worker_sdk.ts'), payloadFile, hookInput.cwd);
After processing, the worker updates the persistent sync state in session-<id>.json to track the last processed transcript entry, ensuring subsequent hooks only process new messages.
Pre-Tool Sync Hook: Injecting Replies into Claude Code
The scripts/pretool_sync.ts script completes the round-trip by pulling Letta replies from the conversation and injecting them back into Claude Code's context. It uses conversation_utils.lookupConversation to retrieve the conversation ID, then fetches new messages via the Letta API.
The script supports two injection modes:
- Whisper mode: Prints the agent's memory blocks and a short header on stdout using
formatAllBlocksForStdout - Full mode: Updates the
<letta>XML section ofCLAUDE.mdusingformatMemoryBlocksAsXmlandupdateClaudeMd
Both modes rely on the conversation ID stored during the SessionStart hook, maintaining continuity across the asynchronous dialogue.
Background Worker and SDK Integration
The actual transmission to Letta happens asynchronously via scripts/send_worker_sdk.ts, which runs as a detached background process to avoid blocking the Claude Code UI.
Letta Code SDK Session Management
Lines 73-85 of send_worker_sdk.ts create a Letta Code SDK session with appropriate tool restrictions based on the sdkToolsMode parameter:
// scripts/send_worker_sdk.ts – SDK session with tool restrictions
const session = resumeSession(payload.conversationId, sessionOptions);
await session.send(payload.message);
for await (const msg of session.stream()) {
// log assistant chunks, tool calls, etc.
}
session.close();
The resumeSession function initializes the connection with the restrictions specified (read-only, full, or off), allowing the Subconscious agent to execute client-side tools when permitted. The SDK forwards the XML payload to Letta, where the agent processes the update and stores its response in the conversation for the next Pre-Tool Sync hook to retrieve.
XML Message Protocol
Claude Subconscious uses a structured XML protocol to communicate state changes:
<claude_code_session_start>: Contains project metadata, path, session ID, timestamp, and SDK tools mode. Sent once when the session initializes.<claude_code_session_update>: Contains the formatted transcript entries since the last sync. Sent after each user prompt.<letta_message>: Wraps the Subconscious agent's responses when injected back into Claude Code.
This XML structure allows the Letta agent to parse session context, distinguish between initialization and incremental updates, and maintain awareness of the current tool permissions.
Configuration and Environment Setup
To enable two-way communication, set these environment variables before running Claude Code:
export LETTA_API_KEY=your-letta-api-key
export LETTA_AGENT_ID=your-agent-id # Optional – auto-imported if missing
export LETTA_MODE=full # whisper | full | off
export LETTA_SDK_TOOLS=read-only # read-only | full | off
When Claude Code runs with the Subconscious plugin installed, it automatically invokes the three hook scripts based on the plugin manifest. No additional code is required to initiate the two-way channel.
Inspecting the Conversation State
The durable conversation mapping lives in <project>/.letta/claude/conversations.json. You can inspect this file to verify the connection:
import { getConversationsFile } from './conversation_utils.js';
import * as fs from 'fs';
const mapPath = getConversationsFile(process.cwd());
const map = JSON.parse(fs.readFileSync(mapPath, 'utf-8'));
console.log('Current conversations:', map);
Additionally, individual session state is tracked in session-<id>.json files, which record the lastProcessedIndex to prevent duplicate message processing.
Summary
- Three-hook architecture:
session_start.tsestablishes the connection,send_messages_to_letta.tspushes updates, andpretool_sync.tspulls responses. - Persistent state: Conversation IDs are stored in
conversations.jsonand session progress insession-<id>.json. - XML protocol: Structured messages (
<claude_code_session_start>,<claude_code_session_update>) carry metadata and transcript data. - Background processing: The
send_worker_sdk.tsworker uses the Letta Code SDK to transmit updates asynchronously without blocking Claude Code. - Dual injection modes: The Pre-Tool Sync hook returns agent replies via stdout ("whisper") or by updating
CLAUDE.md("full").
Frequently Asked Questions
How does Claude Subconscious maintain conversation continuity across Claude Code restarts?
The session_start.ts script checks conversations.json for an existing mapping between the project path and Letta conversation ID. If found, it reuses that conversation; otherwise, it creates a new one. This durable mapping ensures the Subconscious retains context even when Claude Code sessions terminate and restart.
What is the difference between "whisper" and "full" modes in two-way communication?
Whisper mode prints Letta agent responses directly to stdout during the Pre-Tool Sync hook, providing lightweight, ephemeral feedback. Full mode persists the agent's memory blocks into the CLAUDE.md file as XML, making the Subconscious context available to Claude Code's main reasoning loop and enabling deeper integration with the ongoing development task.
Can the Subconscious execute code in Claude Code during two-way communication?
Yes, but only when LETTA_SDK_TOOLS is set to full or read-only. The send_worker_sdk.ts background worker creates a Letta Code SDK session with tool restrictions based on this setting. In full mode, the agent can propose file edits and shell commands; in read-only mode, it can only inspect the codebase; in off mode, it receives updates but cannot invoke client-side tools.
Where are the hook scripts located in the claude-subconscious repository?
The primary hook scripts reside in the scripts/ directory: session_start.ts handles initialization, send_messages_to_letta.ts processes transcript updates, send_worker_sdk.ts manages the background SDK connection, and pretool_sync.ts retrieves agent responses. Utility modules including conversation_utils.ts and transcript_utils.ts support the durable state management and message formatting required for two-way communication.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →