# How Claude Subconscious Manages State Across Multiple Projects with a Single Agent Brain

> Discover how Claude Subconscious manages state across projects using a single agent brain. Learn about durable JSON state storage and central conversation mapping via LETTA_HOME.

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

---

**Claude Subconscious maintains one persistent Letta agent brain across multiple projects by storing durable JSON state in a shared directory managed via LETTA_HOME, allowing any codebase to reference the same conversation ID through a central conversations.json mapping.**

The letta-ai/claude-subconscious repository enables Claude Code to share a single Letta agent across disjoint codebases by managing state through a durable filesystem layer. This architecture allows developers to maintain continuity of context and memory even when working across unrelated projects, using a shared agent brain that persists conversation metadata outside of any individual repository.

## The Shared State Architecture

### Centralized State Directory (LETTA_HOME)

The system establishes a durable state directory through the `getDurableStateDir()` utility in [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts). This function selects a base directory from the `LETTA_HOME` environment variable (falling back to the current working directory) and appends `.letta/claude` to create the persistence layer.

All files that survive across hook runs live under this folder, including the global [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) registry and per-session `session-<id>.json` files. By configuring `LETTA_HOME` to point to the same path across multiple projects, unrelated codebases can access identical state.

### The Conversation Mapping Brain

The [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) file acts as the central brain mapping that connects sessions to persistent conversations. The `getConversationsFile()` function returns the path to this registry, while `loadConversationsMap()` and `saveConversationsMap()` handle reading and writing the mapping structure.

This JSON map stores the relationship between **session IDs**, **conversation IDs**, and **agent IDs**. When `getOrCreateConversation()` executes during a hook run, it first checks the in-memory `SyncState`, then falls back to the persisted map. If a stored entry references a different agent ID, the system clears it and creates a fresh conversation via the Letta API, caching the new conversation ID in both the map and the session's sync state.

## Per-Session State Isolation

### Session-Specific Sync Files

While the conversation mapping is global, per-session state remains isolated through individual JSON files. The `getSyncStateFile()` function constructs paths like `session-<sessionId>.json` within the durable state directory. The `loadSyncState()` and `saveSyncState()` utilities in [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts) manage these files, ensuring each Claude session maintains its own sync checkpoint without interfering with other sessions.

### State Contents and Updates

Each session file stores critical synchronization metadata:

- `lastProcessedIndex`: The index of the last memory block injected into the conversation
- `lastBlockValues`: A snapshot of every block's value from the previous sync operation
- `lastSeenMessageId`: The most recent assistant message already displayed to the user
- `conversationId`: The Letta conversation ID bound to this specific session

When `saveSyncState()` persists updates, it captures the current block payloads and message identifiers, enabling differential syncing on subsequent runs.

## Hook Orchestration and State Flow

The [`scripts/sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/sync_letta_memory.ts) script serves as the hook entry point that orchestrates state management across projects. When executed, it receives `session_id` from Claude's hook input and immediately loads the session-specific sync state via `loadSyncState()`.

If the state lacks a `conversationId`, the script calls `lookupConversation()` to read the shared [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) mapping. After fetching the Letta agent and retrieving new assistant messages, the hook updates `lastBlockValues` with current memory block payloads, sets `lastSeenMessageId` to the newest message identifier, and persists the modified state through `saveSyncState()`. This cycle ensures that every project hook run contributes to and retrieves from the same persistent conversation thread.

## Cross-Project Configuration

### Environment Variables

Three key environment variables control the shared brain behavior:

- **LETTA_HOME** (or **LETTA_PROJECT**): Defines the root directory for the `.letta/claude` state folder. When multiple projects set this to the same path, they share the same [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) and session files.
- **LETTA_MODE**: Determines whether the agent injects full memory blocks (`full`) or only messages (`whisper`), as implemented in the configuration loader within [`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts).
- **LETTA_SDK_TOOLS**: Controls the client-side toolset availability (read-only, full, or disabled).

### Sharing State Across Projects

To share a single agent brain across distinct repositories, configure the environment before running hooks:

```bash

# Project A

export LETTA_HOME=$HOME/.letta_shared
cd /path/to/project-a
npm run claude-hook

# Project B (different repository)

export LETTA_HOME=$HOME/.letta_shared
cd /other/path/project-b
npm run claude-hook

```

Both executions read from and write to the same [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) file, ensuring that the Letta conversation ID remains consistent regardless of which codebase initiated the hook.

## Implementation Examples

### Initializing the Durable Directory

Before first use, ensure the state directory exists:

```typescript
import { ensureDurableStateDir } from './conversation_utils.js';

const cwd = process.cwd();
ensureDurableStateDir(cwd);

```

### Loading or Creating a Conversation

Retrieve an existing conversation or establish a new one for the current session:

```typescript
import {
  loadSyncState,
  saveSyncState,
  getOrCreateConversation,
} from './conversation_utils.js';

async function initializeSession(apiKey: string, agentId: string, sessionId: string, cwd: string) {
  const state = loadSyncState(cwd, sessionId);
  const conversationId = await getOrCreateConversation(
    apiKey,
    agentId,
    sessionId,
    cwd,
    state,
  );
  saveSyncState(cwd, state);
  return conversationId;
}

```

### Updating Per-Session Block Snapshots

After synchronizing memory blocks, persist the current state to prevent redundant injections:

```typescript
function updateBlockSnapshot(state: SyncState, blocks: MemoryBlock[]) {
  state.lastBlockValues = {};
  for (const b of blocks) {
    state.lastBlockValues[b.label] = b.value;
  }
}

```

## Summary

- **Shared Directory**: The `LETTA_HOME` environment variable enables multiple projects to access the same `.letta/claude` state folder.
- **Global Mapping**: [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) maintains the session-to-conversation mapping that constitutes the single agent brain.
- **Session Isolation**: Individual `session-<id>.json` files track per-session synchronization checkpoints without cross-contamination.
- **API Integration**: `getOrCreateConversation()` automatically provisions new Letta conversations when agent mismatches occur.
- **Hook Coordination**: [`sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/sync_letta_memory.ts) orchestrates the full lifecycle from state loading through differential updates to persistence.

## Frequently Asked Questions

### How does Claude Subconscious prevent conversation conflicts between different projects?

The system uses the [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) mapping to associate specific session IDs with conversation IDs. When `getOrCreateConversation()` detects that a stored conversation belongs to a different agent ID than the one currently requested, it automatically invalidates the stale mapping and creates a new conversation. This ensures that while the state directory is shared, each unique session receives the correct conversation context for its associated agent.

### What happens if two projects use the same session ID simultaneously?

Both projects would reference the same `session-<id>.json` file and the same conversation ID within the shared [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) mapping. The per-session state would be overwritten by whichever hook runs last, potentially causing synchronization drift. To avoid this, ensure unique session IDs across projects or isolate projects using different `LETTA_HOME` directories when concurrent isolation is required.

### Can I migrate an existing conversation to a new project?

Yes. Since the conversation metadata lives in the shared [`conversations.json`](https://github.com/letta-ai/claude-subconscious/blob/main/conversations.json) file rather than within any specific project directory, you can point a new project to the same `LETTA_HOME` directory. The `lookupConversation()` function in [`sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/sync_letta_memory.ts) will retrieve the existing conversation ID from the global mapping, allowing the new project to continue the established conversation thread seamlessly.

### What is the difference between LETTA_HOME and LETTA_PROJECT?

Both variables serve as alternative identifiers for the base directory selection in `getDurableStateDir()`. The code checks `LETTA_HOME` first, then falls back to `LETTA_PROJECT` if the former is undefined. If neither is set, the system uses the current working directory. Setting either variable to a shared path enables the cross-project state management described in the single agent brain architecture.