Claude Transcripts docs GitHub
Work in progressUnder active development — not tested as ready for use. Breaking changes land without notice, stored data may need to be discarded between revisions, and there is no auth or security model.

Mirrors

A mirror is a second instance that receives every session as it is recorded, alongside the local one: a laptop that also reports to a home server, several machines feeding one archive, or a local instance in front of a remote system of record.

Configuring one

Add mirrors to the hook runtime config (~/.config/claude-transcripts/config.json, hook-setup.md):

{
  "couch": { "url": "http://127.0.0.1:7652", "databases": { "sessions": "..." } },
  "blob":  { "endpoint": "http://127.0.0.1:7653", "buckets": { "sessions": "..." } },
  "mirrors": [
    { "url": "https://logs.example.com", "timeoutMs": 2000, "blobTimeoutMs": 60000 }
  ]
}
FieldDefaultMeaning
url—Base URL of the mirror's webapi.
timeoutMs2000Cap on each per-event write.
blobTimeoutMs60000Cap on the transcript upload at SessionEnd.
authnoneuser:password if the mirror is behind basic auth.

Every mirror in the array gets every write. No re-registration is needed; sessions already running keep the config they started with. install and setup preserve mirrors when they regenerate the file; edit the key to change or remove it.

Check it:

$ claude-transcripts hook status
hook: settings   ~/.claude/settings.json
hook: config     ~/.config/claude-transcripts/config.json
hook: command    /usr/local/bin/claude-transcripts hook run
hook: mirror     https://logs.example.com
hook: registered for 11 event(s), mirroring to 1

How it works

The hook writes this machine's CouchDB and S3 directly. A remote instance exposes only its webapi, whose store proxies are read-only, so a mirror writes through the one write surface it has, /api/ingest (the same routes import uses):

Written locallySent to the mirror as
event docPOST /api/ingest/events
chunk docPOST /api/ingest/chunks
summary docPOST /api/ingest/summary
<session>/transcript.jsonlPUT /api/ingest/<session>/transcript
<session>/summary.jsonnot sent (a copy of the summary doc)

The mirror applies its own database and bucket names. It is a write path only; the CLI and webui keep reading the local instance.

The timeouts are sized to Claude Code's hook limits: PostToolUse runs on every tool call within a 5-second budget, so a dead mirror must give up well inside it, while the transcript upload happens in SessionEnd's 180-second budget.

Failure behaviour

A mirror never blocks a session or slows the local write: requests run in parallel with the local write, each has its own timeout, and every failure is swallowed.

There is no retry and no queue. Writes that fail while the mirror is unreachable are lost to the mirror; the local copy is complete. Fill the gap from the local instance:

claude-transcripts export ./gap --since 2026-01-01T00:00:00Z
claude-transcripts import ./gap --webapi https://logs.example.com

Import is idempotent, so a range wider than the gap is harmless. To find gaps, compare session lists:

ids() { curl -s "$1/api/sessions?limit=1000" | jq -r '.sessions[].sessionId' | sort; }
ids http://127.0.0.1:7650    > local.txt
ids https://logs.example.com > mirror.txt
comm -23 local.txt mirror.txt    # recorded locally, missing from the mirror

Two silent failure modes: