# Storage ownership

> Who owns which database and folder for Codex and for Claude, why HB Code never opens an agent’s files itself, which tables link a session to its agent, what a session delete removes, and the atomic identity claim behind the outbox.

Handbook / Architecture · Checked against the source on 2026-10-05 · https://www.hbcode.ai/docs/storage

## Separate the stores by owner

Each agent saves its own conversations. HB Code stores the link from a session to the agent’s conversation, the outbox, and live state. It reads a conversation only through the agent’s own interface: the Codex app-server protocol, or the Claude Agent SDK in the agent host.

| Question | Codex | Claude |
| --- | --- | --- |
| Where is the transcript? | In the profile home codex/profiles/<uuid>/home under HB Code’s app data, or at an attached rollout path. | In the main Claude folder under projects. The main folder is CLAUDE_CONFIG_DIR when the desktop has that variable, else ~/.claude. |
| Which identity links the session? | codex_session_links: profile_id, codex_thread_id, and an optional rollout_path. | claude_sessions: native_session_id, which is unique. |
| Who reads history? | The Codex app-server child of the profile, on thread/items/list. | An agent host process that starts for one request. It calls getSessionMessages of the SDK and cuts the page. |
| storageFormat on the wire | indexed | native |
| Where is command output? | The desktop appends the live stream to a file in codex-command-outputs and catalogs it in codex_command_outputs. Output that Codex keeps is read through the app-server. | Only in the transcript. HB Code stores no output byte. Each read of a Bash result sends the host request toolOutput. |
| What does HB Code store itself? | codex_profiles, codex_session_links, codex_command_outputs, codex_command_output_pending_appends, and the output files. | claude_sessions, claude_operations, the folders of added accounts, and the active account ID in app_settings. |

**Each agent keeps its own transcripts, and HB Code reaches them only through that agent**

One computer with three processes on the left and the folders they open on the right. HB Code’s app data folder holds appdata.db, the command output and attachment files, a home folder for each Codex profile, and a folder for each added Claude account. The desktop runtime opens its own database and files. It creates and deletes the Codex profile folder and never opens a file inside the home. Only the Codex app-server opens the home, and it opens an attached rollout at any other path when a request names it. A Claude account folder keeps the sign-in entries of its account, and every other entry is a link that leaves the app data folder and points into the main Claude folder. The Claude agent host runs with the active account’s folder, and every history read runs with the main folder.

- **HB Code data** (appdata.db · codex-command-outputs/ · pasted-images/): The desktop runtime opens these itself: SQLx for the database, plain file reads and writes for Codex command output and chat attachments.
- **Codex home** (codex/profiles/<uuid>/home/): HB Code creates the folder, launches Codex with it as CODEX_HOME, and deletes the whole profile folder when you remove the profile, unless a session still links a rollout inside it. It never opens a file inside. History comes back through thread/items/list.
- **Startup guard** (--allowed-storage-root = CODEX_HOME = sqlite_home): The bundled app-server refuses to start unless all three resolve to the same folder, so Codex can’t keep its databases anywhere else.
- **Claude account folder** (claude/accounts/<uuid>/): One folder for each account you add. It keeps .claude.json, its lock file, .credentials.json, backups, and statsig. HB Code links every other top-level entry of the main folder into it and never replaces an entry that the folder already has.
- **Main Claude folder** (~/.claude · CLAUDE_CONFIG_DIR): Claude Code owns it. All accounts save their transcripts here through the projects link. The agent host reads them with the Claude Agent SDK, and every history read starts the host with this folder, whichever account is active.

The figure shows the desktop computer only. Two stores are off the desktop: the server database and the phone. The last two rows of the next table name what each holds. Neither holds a transcript.

| Store | Owner and content | Access rule |
| --- | --- | --- |
| HB Code appdata.db | Desktop product metadata, session links for both agents, outbox operations, remote idempotency, live overlay state, and plan-limit readings. | HB Code Rust repositories use SQLx and SQLite. |
| App-owned Codex profile home | The Codex child owns its private databases, auth/config, and normal owned session storage. | Use the app-server protocol. HB Code must not query the child’s private SQLite schema. |
| An explicitly selected Codex conversation source | The exact linked transcript path and thread identity define the source. | Access the selected source through path-aware app-server methods. Do not search other homes for a replacement. |
| Main Claude folder (~/.claude, or CLAUDE_CONFIG_DIR when it is set) | Claude Code owns its saved transcripts, settings, and sign-in. | The desktop runtime never parses a transcript. The agent host reads transcripts through the Claude Agent SDK. |
| Claude account folder (claude/accounts/<uuid> in app data) | The sign-in of one added account: .claude.json, its lock file, .credentials.json, backups, and statsig. All other entries, projects included, are links into the main folder. | HB Code creates the folder and the links and removes the folder with the account. Removal deletes the links and never follows them. |
| Server PostgreSQL | Account and service data controlled by server repositories and database roles. | Separate migration, system-runtime, and tenant-runtime principals. |
| Phone state | View state, local copies of unsent messages, and preview and output caches. | Treat the desktop as authority for projects, execution, outbox, and files. |

Every Claude transcript read runs with the main folder, whichever account is active. A saved transcript needs no sign-in, and every account saves into the same projects folder. HB Code creates that folder in the main folder before it links an account. If the projects link fails, the account setup fails with an error. A failed link for any other entry only writes a warning to the log.

The agent host opens three kinds of Claude files directly, because the SDK does not return them: the meta.json file beside a subagent transcript, the workflow run record, and the effort field of a subagent’s first saved assistant turn. It reads them and never writes them.

The WebView asset protocol is scoped to media previews under the app cache only, so no private runtime file is reachable from the page.

- [Claude account folders](https://www.hbcode.ai/docs/claude-agent-sdk.md#accounts): How an account is added, which folder a run uses, and how usage is read.
- [Command output](https://www.hbcode.ai/docs/output-and-previews.md#artifacts): The Codex artifact file and the Claude read from the transcript.

## Storage identity and execution identity differ

The column sessions.chat_agent holds codex or claude. It is set when the session is created, and a session update keeps it. It decides which identity tables a session uses.

**A session row links to exactly one agent’s identity tables**

Two sessions side by side in appdata.db. The Codex session has one row in codex_session_links that names a profile, a thread, and an optional rollout path, and the profile row points at the profile home on disk. The Claude session has one row in claude_sessions that names the Claude session ID, and one row in claude_operations for each message it sent, which names the UUID of that user message in the transcript. A session never has rows on both sides.

- **sessions.chat_agent** (codex · claude): Set when the session is created and not changed by a later update. It decides which side a session has. The column default is codex.
- **codex_session_links** (app_session_id → profile_id, codex_thread_id, rollout_path, status): A partial unique index allows one available link for each profile and thread. The status is available, profile_required, or unavailable. The profile_id foreign key restricts deletion of a profile row that a link still names.
- **claude_sessions** (app_session_id → native_session_id (unique)): One app session owns one Claude conversation. A second session that tries to continue the same conversation fails with “This Claude conversation is already open in another session.”
- **claude_operations** ((app_session_id, operation_id) → native_message_id, run_id, display_text, attachments): Written before the send. A retry of the same operation keeps the first UUID and the first run ID. A retry with a different text or different attachments fails with a conflict.

An app-owned Codex profile has a stable opaque UUID in canonical lowercase text. An email address and a filesystem path are not profile identity. The codex_profiles table holds one row for each profile with a status of signed_out, authenticated, or unavailable and the account columns that the app-server reported.

Indexed and Exact are design names for the two Codex read routes. In code one predicate decides: a link whose rollout path lies outside the runtime home is an external exact source. Both routes call thread/items/list on the Codex app-server. An exact read adds path and historySources. The wire storageFormat of a Codex session stays indexed on both routes. An operation resolves its route once and keeps it.

A Claude session has no profile link, because every Claude account reads the same transcripts. HB Code does not store whether a transcript exists. The host answers missing for a conversation that Claude has not saved yet, and the desktop then shows an empty timeline. The UUID in claude_operations is the UUID of the user message that the desktop sends, which is how the desktop finds that message in the transcript. The row also keeps the message text and the attachments.

> **Do not build a transcript scanner** The storage contract forbids automatic reads of external Codex homes and direct transcript parsing in the HB Code backend. A user-selected source goes through the app-server protocol. A missing or invalid source is an explicit error, not a reason to copy or reconstruct another conversation.

## Exact source access has deliberate limits

**An exact read builds history from byte ranges of the files that the desktop lists**

For an exact source the desktop sends the rollout path and the session’s other rollouts as historySources. The Codex app-server replays the parent’s bytes up to the offset the child recorded, then the child’s own bytes, and binds the cursor to a digest of exactly those bytes. Appending to the child keeps the cursor valid, and rewriting read bytes forces a reload. A parent that isn’t listed fails the read, even when its file sits in the same folder. Sizes are examples.

- **historySources** ([{ rolloutId, path }]): Built by the desktop from the absolute paths of the session’s other linked rollouts, which it records as Codex announces each child. It’s the only place the app-server looks for an ancestor.
- **history_base** (thread_id · end_byte_offset): Session metadata at the top of the child’s file: the ancestor rollout and the byte where the inherited part ends. The app-server follows up to 256 ancestors and rejects a cycle or an ancestor shorter than that offset.
- **reloadRequired** (error data.code): The cursor carries a digest of every byte it covered. Appending keeps it valid. Changing those bytes fails the next page, which the desktop shows as an ordinary load error. Only sourceUnavailable offers to choose the source file again.

| Condition | Contract |
| --- | --- |
| Required ancestor source is absent | The app-server fails the read with missingHistorySource, even if the file sits next to the child. The desktop shows a generic load error. Only sourceUnavailable becomes CODEX_SOURCE_ERROR, which offers to choose the source file again. |
| Source is replaced or truncated | The app-server binds each cursor to a digest of the bytes it read. Appending keeps cursors valid. Truncating or rewriting fails the next page with reloadRequired, shown as a generic load error. “Timeline source changed; reload the timeline” is the desktop’s error when the linked source itself changed. |
| Current method takes only a thread ID | Do not offer that mutation for an exact external source if it cannot address the selected path. This applies to current goal, rollback, and persisted thread-settings operations. |
| Another bundled sidecar owns the writer | Release thread ownership before a different runtime resumes it. |

Before a send or an attach, the desktop releases the thread from every other profile server it runs. That coordinates the bundled sidecars of this process. It does not prevent an unrelated external Codex process from writing the same source. Stop an external writer before you continue that conversation in HB Code.

## The outbox uses an atomic identity claim

Key relationships in the product database (schematic, not migration SQL):

```
workspace_chat_outbox
  session_id → entries_json, updated_at

workspace_chat_outbox_operation_identity
  (session_id, operation_id) → request_fingerprint

remote_rpc_idempotency
  scope_key → request_fingerprint, state, response_json, lease, expiry

chat_operation_ledger
  (app_session_id, operation_id) → status, result_json

chat_session_messages
  operation_id → recipient, sender, sender_call_id, sender_run_id, sender_thread_id, chain_id, hop, delivery

chat_live_timeline_overlays
  (app_session_id, timeline_target_id) → revision, snapshot_json

chat_live_timeline_overlay_revisions
  singleton → one global revision sequence

chat_pending_terminal_overlay_cleanups
  (app_session_id, run_id)

agent_usage_samples
  agent, account_key, window_kind → used_percent, resets_at, sampled_at
```

Five tables carry the chat_ prefix, and they serve Codex and Claude sessions alike. Four of them had the codex_ prefix until a rename migration. chat_session_messages is newer. Tables that only Codex uses keep the codex_ prefix, for example codex_session_links and codex_command_outputs.

| Table | Content | Rule |
| --- | --- | --- |
| chat_operation_ledger | The status and the stored result of one chat operation. | Key: app session ID and operation ID. A session delete removes the rows of the base session, of its ::skills-agent alias, and of the alias descendants. |
| chat_session_messages | The record of one message that an agent in one session sent to another session. | Key: operation ID. The pair of sender session and sender call ID is unique, and hop is at least 1. delivery is sent, needs-approval, or declined. |
| chat_live_timeline_overlays | The saved live overlay of one timeline as JSON. | Key: app session ID and timeline target ID. For Claude the target is the app session ID for the conversation and the subagent ID for a subagent. |
| chat_live_timeline_overlay_revisions | One row with one revision counter for all overlays. | The counter only grows, up to 2^53 − 1. A new target cannot reuse the revision of a deleted target. |
| chat_pending_terminal_overlay_cleanups | The ended runs whose live rows are not yet fully replaced by saved history. | Key: app session ID and run ID. The row is written before the desktop forgets the active run and stays until every timeline of that run is reconciled. |

save_workspace_chat_outbox_with_operation_identity opens one transaction. Its INSERT … ON CONFLICT updates the identity row only when the existing fingerprint equals the submitted fingerprint. If that statement does not affect one row, the transaction rolls back. It then upserts the outbox entries JSON. For a message from another session it also inserts the chat_session_messages row, and a row with the same operation ID stays as it is. Then it commits. A session message is never queued without its record and never recorded without its queue entry. The identity row is keyed by the base session while the outbox JSON is saved under the alias session, so the same operation can be found from any alias in the scope.

The remote_rpc_idempotency table is the second ledger. It keeps the outcome of every listed remote mutation for 48 hours across desktop restarts, so a phone that retries after the desktop came back still gets the stored answer.

agent_usage_samples keeps plan-limit readings for both agents. The account key is the active Codex profile ID, or the Claude account ID, which is default or a UUID. A reading is skipped when the newest reading of the same window is younger than 10 minutes, has the same reset time, and differs by less than 0.5 percent. Readings older than 35 days are deleted.

- [RPC replay and idempotency](https://www.hbcode.ai/docs/relay-protocol.md#idempotency): The claim states and what the router does with each.
- [Messages between sessions](https://www.hbcode.ai/docs/message-delivery.md#session-messages): How an agent sends a message to another session and what the record prevents.

## What a session delete removes

delete_session_with_scoped_references removes the HB Code rows of a session in one SQLite transaction. The delete sends no request to Codex and none to Claude. The agent’s transcript stays where the agent saved it.

| Data | Effect of the delete |
| --- | --- |
| codex_command_outputs rows of the session | Deleted in the transaction. Their pending append rows go with them. The output files are removed after the commit. |
| chat_operation_ledger | Rows of the base session, of its ::skills-agent alias, and of the alias descendants are deleted. |
| codex_session_links | Rows with this base session are deleted. |
| workspace_chat_outbox | Rows of the same three scopes are deleted. |
| project_workspace_state | active_session_id becomes NULL where it named the session. The row stays. |
| sessions | The row is deleted last. |
| claude_sessions, claude_operations, workspace_chat_outbox_operation_identity, chat_session_messages, chat_live_timeline_overlays, chat_pending_terminal_overlay_cleanups | Deleted by ON DELETE CASCADE from the sessions row. A session message record goes when its sender or its recipient is deleted. |
| Memory of the running desktop | After the commit the desktop clears the cached outbox entries, the video analysis entries, and the in-memory operation ledger of the session. |
| Codex thread and rollout files | Never touched. They stay in the profile home or at the attached path. |
| Claude transcript | Never touched. It stays in the main Claude folder, and a new session can continue it. |

## Project file access is a separate boundary

Remote file reads run with the desktop user’s filesystem access. resolve_desktop_file_read_path expands a home shortcut, joins a relative path onto the canonical project directory, and canonicalizes the result. It does not enforce project containment, and the design note says so on purpose. allowExternalFile and allowExternalMedia only decide which resolver is tried first, so an authenticated companion can open paths outside the selected project either way. ensure_path_within_project, which rejects parent components and checks the canonical root, exists for desktop-local operations.

App-managed chat attachments live in pasted-images under the app data directory, and an external media read for an attachment must classify as image, video, audio, or document. The phone never uses its own home directory.
