Skip to content

Archive layout

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:

Terminal window
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/
├── 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 features. They contain credentials and, for profiles, the harness’s own history.

plugins/ and worktrees/ exist only once a project environment uses them. Each worktree is checked out on a new hansard/ branch, and Hansard never removes one.

<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.

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

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.

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

Section titled “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.

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.

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

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.

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 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.