Install-ready onboarding + automatic session receipts - #1
Merged
Conversation
…ocal-first framing)
Make Lotor safe and legible to install as a standing (user-global) MCP server, plus a guided first run. Canonical home: - new src/home.js resolveHome() = LOTOR_HOME env, else ~/.lotor, wired into every entry point (MCP server, ingest, gate/view/approve CLIs) so the receipt chain and keys no longer bind to the launch cwd. Fixes per-directory chain fragmentation and private-key scatter under a user-global install. Library baseDir defaults left at '.'. - views integrity check reads the pubkey from the resolved home, so `npm run receipts` no longer falsely reports "Chain BROKEN". First-run onboarding: - bin/setup.js (npm run setup): ensures the chain key, walks the owner through the approval passphrase, prints a LOTOR IS LIVE panel. The already-set-up path needs no TTY. - lotor_status MCP tool: reports home, receipt count, chain integrity, and whether the human-signature gate is set up; guides first-run setup. - gated_action returns a setup hint when the gate is uninitialized. Docs: - README Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, and how to confirm it is live. Tests: 70 passing (added home + status suites; existing suites isolated to a temp LOTOR_HOME so runs never touch the real ~/.lotor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a user_config directory field (lotor_home) to the .mcpb manifest and map it to the server's LOTOR_HOME env var. Claude Desktop now asks the user where receipts live at install time, with the local-disk implication written into the field description. Defaults to ~/.lotor, matching the runtime default in src/home.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the gap between "Lotor is installed" and "Lotor is recording". - bin/hook-session-end.js: a Claude Code SessionEnd hook target. Reads the hook payload from stdin (transcript_path, or transcriptPath), or takes a path as argv[2] for manual runs. Dedupes by session id against the existing chain before appending. It cannot break the user's session: never exits non-zero, never writes stdout, catches every failure mode and reports one line to stderr. The stdin read has an unref'd timeout so a hook host that opens stdin without writing cannot hang shutdown. - parser: session.startedAt/endedAt now scan for the first and last entry carrying a usable timestamp and accept `timestamp` as well as `createdAt`. Real Claude Code transcripts use `timestamp` and can open or close with metadata lines, so both fields were coming out null on real sessions. - README: install step 6 documents registering the hook, plus the manual ingest fallback. - KNOWN-LIMITS: document first-write-wins session receipts (no supersede on resume) and that auto-capture is opt-in on the hook being registered. Verified against a real Claude Code transcript: receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files and outbound items. Second run dedupes. Tests 82 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hook now writes on every session end, so concurrent writers are real. Two read-then-write races could corrupt the chain. 1. appendReceipt computed seq and prevHash from a tail read at store construction. Two processes appending at once produced duplicate seq values and a chain that fails verifyChain. Reproduced: 8 concurrent appends yielded seq [0,0,0,0,0,0,0,0]. 2. loadOrCreateKeyPair had the same shape. N processes on a fresh home could each generate a different keypair, last writer wins, and entries signed by the losing key no longer verify. Reproduced: "signature verification failed at entry 0". - src/store/lock.js (new): synchronous withLock(baseDir, fn). Exclusive create on <home>/receipts/.chain.lock, bounded ~5s busy-wait, 30s stale-lock steal so a crashed process cannot deadlock every later session, release in finally. Treats EEXIST/EPERM/EACCES/EBUSY as contention: Windows returns EPERM, not EEXIST. - appendReceipt now re-reads the chain from disk INSIDE the lock and seeds a fresh chain from that state. Locking only the write is not enough. - loadOrCreateKeyPair uses double-checked locking: the fast path (keys present) stays lock-free, the slow path re-checks inside the lock. Both concurrency tests use a wall-clock barrier so children genuinely overlap. Without it they pass against the buggy code, which was observed and corrected during development. Each test was verified to fail against the unfixed implementation. Tests: 88 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Honesty about limits is this document's stated job, so these belong in it rather than in a commit message or an issue. - 7. A receipt can be dropped under lock contention. The chain lock waits a bounded ~5s and gives up; the SessionEnd hook exits 0 on that path because wedging the editor is worse than a missing receipt. Announced on stderr, but nothing retries. The chain stays valid. - 8. The chain signing key is stored unencrypted. The approval key is never on disk (derived from the passphrase at sign time), but keys/chain.key is plaintext. Anyone who can read it can forge entries that verify. Tamper-evidence assumes an attacker who can alter the log cannot read the key, which is weak on a compromised machine. - 9. The chain key is per-machine, so receipts do not verify elsewhere without copying the chain public key. The approval key is portable because it is passphrase-derived. - 10. Approval key strength is entirely passphrase strength. PBKDF2 uses a compile-time constant salt, so the same passphrase yields the same key across installs and precomputation is shared. A per-install random salt would be a breaking change to key derivation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes Lotor safe and legible to install as standing infrastructure, then closes the gap between "installed" and "actually recording".
Why
Installing the MCP server user-globally exposed two problems. The store bound to the launch directory, and nothing ever wrote a receipt. Both are fixed here, along with a guided first run so a new user knows the thing is alive.
Canonical home
The MCP server and every CLI resolved
receipts/andkeys/relative to the process working directory. Under a user-global install that fragments the chain into a separate log per project directory and scatters the private chain key into arbitrary repos.src/home.jsresolveHome():LOTOR_HOMEif set, else~/.lotor.baseDirdefaults stay'.'so unit tests are unaffected../keys, sonpm run receiptsfalsely reported "Chain BROKEN" from anywhere but the home dir. Now threads the resolved home.First-run onboarding
npm run setup(bin/setup.js): ensures the chain key, explains and walks the owner through the approval passphrase, prints a status panel. The already-set-up path needs no TTY.lotor_statusMCP tool: home path, receipt count, chain integrity, and whether the human-signature gate is set up. Its description tells the assistant to surface it on first run and guide the user through setup.gated_actionreturns a setup hint when the gate is uninitialized.Automatic session receipts
bin/hook-session-end.js: a Claude CodeSessionEndhook target. Reads the hook payload from stdin, dedupes by session id, appends a receipt.It is deliberately incapable of breaking a session: never exits non-zero, never writes stdout, catches every failure with a single stderr line. The stdin read carries an unref'd timeout so a hook host that opens stdin without writing cannot hang shutdown.
session.startedAt/endedAtnow scan for the first and last entry with a usable timestamp and accepttimestampas well ascreatedAt. Real Claude Code transcripts usetimestampand can open or close with metadata lines, so both fields were coming outnullon real sessions.Docs
.mcpbmanifest declaresuser_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.Verification
Known follow-ups (not in this PR)
Distribution is still a git-checkout model, so the MCP config points at wherever the repo was cloned. Gate setup requires a terminal, which blocks GUI-only Desktop users from the signature feature. The passphrase prompt uses raw-mode stdin and is untested on Git Bash / MinTTY.