Transcript transport (pluggable)¶
The SessionEnd hook relays a session's artifacts off each machine. The how is abstracted
so the backend can change without touching the hook. Three things are shipped, all via the
same backend (so sensitive content never takes a separate third-party path):
| Artifact | Source | Lands at |
|---|---|---|
| transcript | ~/.claude/projects/<slug>/<session>.jsonl |
<host>/<slug>/<session>.jsonl |
| subagents (subagent transcripts, tool-results) | ~/.claude/projects/<slug>/<session>/ |
<host>/<slug>/<session>/ |
| plans (sensitive; not session-keyed) | ~/.claude/plans/ |
<host>/plans/<date>_<topic>.md |
CLAUDE.local.md (personal project rules; host-specific) |
<project working tree>/CLAUDE.local.md |
<host>/<slug>/CLAUDE.local.md |
The layout is uniform: <host>/ + the same path Claude uses locally. (Memory is the one
exception — it drops the host prefix because it's shared across machines, not archived per-host;
see memory-sync.md.) CLAUDE.local.md is host-specific, so it's a one-way per-host archive, not
merged like memory.
transport_ship <src> <relpath> accepts a file or a directory. (Memory is not shipped
here — it rides the scrubjay-data git sync.)
How it's wired¶
SessionEnd hook (hooks/log-session.sh)
└─ bin/ship-transcript.sh <transcript> <slug> <session> <host>
└─ hooks/transports/<backend>.sh :: transport_ship <src> <host/slug/session.jsonl>
Backend is chosen by SCRUBJAY_TRANSCRIPT_BACKEND in ~/.config/scrubjay/config.
A backend is one file defining transport_ship <src> <relpath>.
Backend: git (GitHub — zero infrastructure)¶
Copies the transcript into the scrubjay-chats private repo (SCRUBJAY_CHATS) and pushes
it. This is the parallel to the peer-to-peer path for anyone who'd rather not run a NAS:
GitHub is the shared store, and no WireGuard or receiver is involved. Optionally, a
mirror host can also pull the repo down to a NAS (mirror-host.md), but that's an add-on
— the private repo is a fine permanent home on its own.
⚠️ Tradeoff: transcripts transit and live on GitHub's servers. The repo is private, but
it's still a third party — that's the price of not running your own storage. If that
matters to you, use rsync-wg/local instead.
Backend: rsync-wg (peer-to-peer, no third party)¶
Each machine holds a per-machine SSH key and rsyncs over a WireGuard/SSH tunnel directly to the NAS receiver. No data on any third-party server.
Set in ~/.config/scrubjay/config:
SCRUBJAY_TRANSCRIPT_BACKEND="rsync-wg"
SCRUBJAY_WG_TARGET="scrubjay-rx@scrubjay-receiver" # ssh destination ONLY — no remote path
SCRUBJAY_WG_SSHKEY="$HOME/.ssh/scrubjay_transcripts_ed25519"
⚠️ No remote path in SCRUBJAY_WG_TARGET. The receiver authorizes the key with a forced
command="<APP>/bin/sj-receive.sh <root>" (which runs rrsync -wo <root> then chmods the
landed files group-readable — see below), which pins the destination root; everything the client sends is
taken relative to it. Including :/srv/scrubjay-chats would make rrsync re-root it under
itself (/srv/scrubjay-chats/srv/scrubjay-chats/…). Per-machine reachability — HostName, the
(often non-standard) SSH Port, User — lives in a ~/.ssh/config scrubjay-receiver
alias, so this config line is identical on every machine. bin/onboard.sh writes both.
Receiver requirements (see the private runbooks/wireguard-transcripts.md):
- a restricted command="<APP>/bin/sj-receive.sh <root>",restrict line per machine key —
sj-receive.sh wraps rrsync -wo <root> (write-only, no shell) and, after each push, chmods
the new files group-readable so the archive owner (human + MCP server) can read what the relay
account wrote (the relay writes 0600; setgid stamps the shared group, this adds group-read);
- the rrsync user must be able to traverse to the NAS root (e.g. setfacl -m u:scrubjay-rx:x
on a 0750 parent like /media/<user>);
- rsync ≥ 3.2.3 on both ends (for --mkpath).
Backend: local (the box that is the NAS)¶
The receiver itself (the machine with the NAS mounted) shouldn't rsync to itself over WG.
The local backend just copies the transcript straight into the NAS chats root:
SCRUBJAY_TRANSCRIPT_BACKEND="local"
SCRUBJAY_LOCAL_CHATS="/mnt/nas1/scrubjay-storage" # the NAS storage root
Layout matches every other backend (<host>/<slug>/<session>.jsonl), so a local sender and
the WG receivers all write into one tree. Lives in hooks/transports/local.sh.
Adding any other transport (e.g. S3, syncthing) is just another
hooks/transports/<name>.sh defining transport_ship.