# Archive layout

> Where Hansard stores conversations, original files, attachments, memories, and indexes, and which version fields control rebuilds.

Hansard keeps its state in the Hansard home, `~/.hansard` by default, and the
archive in `<storage.root>/hansard`, which defaults to `~/.hansard/data/hansard`.
Check the active paths instead of assuming the defaults:

```sh
hansard status
hansard config path
hansard config get storage.root
```

Set `HANSARD_HOME` to run Hansard with a separate home. It must be an absolute
path, or start with `~` or `~/` for your home folder. Hansard refuses a
relative path, which would put an archive in whatever folder a command runs
from. `storage.root` can put the archive on another local disk.

Without `HANSARD_HOME`, Hansard places `.hansard` under `HOME`, then
`USERPROFILE` if `HOME` is unset. It uses the operating system home only when
neither variable is set. This applies to the CLI on every operating system.

Hansard creates its directories and files readable only by your user. A copy
you move elsewhere takes on the permissions of its new location.

## Hansard home

```text
~/.hansard/
├── config.json          settings
├── redaction.yaml       redaction rules
├── state/               watcher, import, sync, and integration state
├── spool/               pending work
├── cache/               derived caches, and the reveal cache of messages before redaction
├── logs/                watcher and worker logs
├── account-profiles/    harness sign-in profiles Hansard manages
├── account-proxy/       the managed request router and its credentials
├── plugins/             plugins and marketplaces Hansard keeps for project environments
├── worktrees/           a git worktree for each conversation an environment with worktrees on starts
└── data/
    └── hansard/         the archive
```

`account-profiles/` and `account-proxy/` exist only when you use those
[account](https://www.hansard.dev/guides/account-authentication/) features. They contain
credentials and, for profiles, the harness's own history.

`plugins/` and `worktrees/` exist only once a
[project environment](https://www.hansard.dev/guides/projects/) uses them. Each worktree is
checked out on a new `hansard/` branch, and Hansard never removes one.

## Archive root

```text
<storage.root>/hansard/
├── sessions/        conversations, one partition per repository or scope
├── memory/          the memory archive
├── objects/         content-addressed copies of original files, attachments, and artifacts
├── raw/             preserved web exports
├── raw-sources/     shared snapshots from older releases
├── indexes/         search and list indexes
├── received/        read-only copies from other devices and Teams
├── capabilities/    skill and plugin revision history
├── chat/            Deep Recall research threads
├── orchestrate/     helper-agent and handoff records
├── telemetry/       Claude Code activity metadata from the local collector
└── evals/           imported plugin evaluation results
```

`indexes/`, `received/`, `capabilities/`, `chat/`, `orchestrate/`,
`telemetry/`, and `evals/` never upload through Personal Sync or Teams.

`telemetry/claude-code.sqlite` keeps times, token counts, and tool names from
Claude Code, never prompts, replies, or commands, and drops rows older than 14
days. Earlier versions wrote raw JSON under `telemetry/year=*/`. Nothing reads
those files now, and Hansard leaves them in place.

## Session partitions

Conversations are partitioned by repository key, or by scope for conversations
without a repository, then by harness and start date:

```text
sessions/
└── repo=<encoded-repository>/
    └── provider=<provider>/
        └── year=YYYY/
            └── month=MM/
                └── day=DD/
                    ├── session=<session-id>.metadata.json
                    ├── session=<session-id>.transcript.jsonl
                    ├── session=<session-id>.events.jsonl
                    ├── session=<session-id>.event-record.jsonl
                    ├── session=<session-id>.conversation.md.gz
                    ├── session=<session-id>.view.json
                    ├── session=<session-id>.view.pages/
                    ├── session=<session-id>.view-pages/
                    ├── session=<session-id>.attachments/
                    ├── session=<session-id>.artifacts/
                    └── session=<session-id>.raw/
```

The repository key is percent-encoded, so `github.com/example/app` becomes
`repo=github.com%2Fexample%2Fapp`. A folder without a Git remote gets a key
based on its first commit or its path. Web chats and conversations without a
repository use `scope=<encoded-scope>` in place of `repo=`.

The `.event-record.jsonl`, `.view.pages/`, `.view-pages/`, `.attachments/`,
`.artifacts/`, and `.raw/` entries exist only when a session needs them.

## Session files

| File                   | Contents                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `*.metadata.json`      | Identity, harness and source type, repository or scope, working directory, device and person, start and end times, message counts, models, token usage, tool usage, and version fields.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `*.transcript.jsonl`   | Normalized messages, one JSON object per line, with redaction applied. Under `archive.format` `2`, an import keeps the lines the file already shares with the new transcript and writes only the rest in place, as it does for the originals the reveal cache keeps; it rewrites the file whole when an earlier line changed. Readers leave out a last line that an interrupted update left without its newline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `*.events.jsonl`       | Canonical events built from the transcript: prompts, tool calls and results, edits, and similar records.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `*.event-record.jsonl` | Canonical events kept as references into the events derived from the transcript, in place of `*.events.jsonl`. Hansard derives the events from the transcript prefix the record names and checks each one against it, and derives them in memory when the record no longer matches. Imports write this form when `archive.format` is `2`, the default, and every copy of Hansard that may open the archive reads it, for sessions whose plain events would exceed 1 MiB.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `*.conversation.md.gz` | A readable Markdown rendering used by `hansard show` and exports, stored gzip-compressed under archive format 2 and as plain `*.conversation.md` under format 1, which imports write while an earlier Hansard release that cannot read the compressed file may open the archive. Commands that name the file (`hansard show --path`, `hansard history`, `hansard status`) name a plain `conversation.md` copy under `~/.hansard/cache/conversation-markdown/`, which cache cleanup removes after 30 days unused, and exports and uploads carry it as plain `conversation.md`. Run `hansard rebuild --since all` once to compress the plain files earlier versions kept. While a conversation is live, the watcher writes it at most every two minutes and again once the conversation settles, so the file can trail the transcript by that much; `hansard show`, `hansard history`, `hansard export`, and the app's Markdown view render it before they read it or name its path, waiting at most a few seconds for a running import. A path still behind, as windowed reads such as `hansard show --json --msg-limit 20` name it, is marked `conversation_current: false`. |
| `*.view.json`          | The prebuilt payload the app renders. Very large sessions add compressed pages under `*.view.pages/`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `*.view-pages/`        | A view kept only as compressed pages, in place of `*.view.json`. Hansard reads the pages while their manifest matches the session's transcript and the session times they show, takes the rest of the header from the session's metadata, and `hansard export` writes them out as a plain `view.json`. Pages that no longer match are written again by the first read that finds no import running. Imports write this form when `archive.format` is `2`, the default, and every copy of Hansard that may open the archive reads it, for sessions with more than 1000 messages or a view above 1 MiB.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

The metadata, transcript, and events files (`*.events.jsonl` or `*.event-record.jsonl`) are the source for listing, search,
Stats, and every rebuild. The view files can always be rebuilt from them.

Conversation navigation reads its committed list index without checking every
session file on the request. A bounded background worker validates the files
when archive files or a received copy change while the list index does not,
after an interrupted write, and at least every ten minutes, and reconciles
changes made outside Hansard after active imports finish. Imported sessions
continue to appear from the committed index. This change does not require a
reimport.

## Original files, attachments, and artifacts

| Sidecar               | Contents                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `*.raw/manifest.json` | One record per original harness file the importer read: original path, size, modification time, SHA-256, how it was preserved, and any privacy projection. Each record points to the stored copy, usually under `objects/raw/` and sometimes beside the manifest. With `archive.rawStorage` `chunked`, the record of a shared SQLite store's snapshot points instead to a recipe under `objects/raw/` that lists its compressed 1 MiB chunks, and the manifest is version 4. |
| `*.attachments/`      | User and tool attachments, with an `attachments.json` manifest. Transcripts hold short descriptors instead of inline data.                                                                                                                                                                                                                                                                                                                                                   |
| `*.artifacts/`        | Screenshots and PDFs referenced by tool calls, captured only when `artifacts.enabled` is `true`.                                                                                                                                                                                                                                                                                                                                                                             |

`objects/<namespace>/sha256/<first-two-hex>/<hash>` stores one copy of each
file, shared by every session that references it. The namespaces are `raw`,
`attachment`, and `artifact`. Web exports are kept once under
`raw/web-exports/` and referenced by each imported conversation.

> **Original files are not redacted:** Redaction applies to transcripts, events, Markdown, and views. Original
> harness files keep their exact bytes, except OpenCode and MiMo Code copies,
> which leave out stored credentials and record that in the manifest. Protect
> `objects/`, `raw/`, and any `hansard export --include-raw` output like the
> harness history itself.

## Memory archive

```text
memory/
└── repo=<encoded-repository>/
    └── provider=<provider>/
        └── source=<source>/
            └── item=<memory-id>/
                ├── item.json
                └── snapshots/
```

Global memories use `scope=global` in place of `repo=`. `item.json` records the
memory's origin, current content hash, metadata, and snapshot history.
`snapshots/` holds a full copy of each version, written only when the content
hash changes. The `origin` field is `file` for editable harness files,
`provider-cloud` for read-only cloud memories, and `manual-import` for imported
memories.

## Received copies

Receiving never changes the local archive. It adds read-only copies:

```text
received/personal/<profile-id>/devices/<device-id>/...
received/teams/<team-id>/members/<member-id>/devices/<device-id>/...
received/remotes/<destination-id>/devices/<device-id>/...
```

Received sessions keep their original source IDs and get their own catalog
IDs. Hansard builds their views and events in the local cache, leaving the
received files untouched. Older `received/devices/` and `received/members/`
folders stay readable as they are; receive never moves or prunes them.

## Version fields

Each `*.metadata.json` records the versions that produced it. A mismatch with
the running code tells Hansard what to rebuild.

| Field                         | Covers                                              | Refresh                                                                          |
| ----------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------- |
| `archiveSchemaVersion`        | Folder layout and metadata shape                    | Reimport, when [release notes](https://www.hansard.dev/releases/) call for it    |
| `viewSchemaVersion`           | `*.view.json`, `*.view.pages/`, and `*.view-pages/` | Automatic on read, or `hansard rebuild`                                          |
| `canonicalEventSchemaVersion` | `*.events.jsonl` and `*.event-record.jsonl`         | `hansard rebuild`                                                                |
| `parserVersions`              | The importer logic for each source                  | `hansard rebuild --stale-parsers`, or `hansard import --source <id> --since all` |

`memory_schema_version` in `item.json` covers the memory record shape. Search
indexes have their own version and rebuild with `hansard index rebuild`.

When a release requires a complete local reimport, preview it with
`hansard update --dry-run`, then run `hansard update`. See
[Maintenance](https://www.hansard.dev/reference/maintenance/).
