Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Install-ready onboarding + automatic session receipts - #1

Merged
githubscum merged 7 commits into
mainfrom
feat/session-receipts
Jul 22, 2026
Merged

Install-ready onboarding + automatic session receipts#1
githubscum merged 7 commits into
mainfrom
feat/session-receipts

Conversation

@githubscum

Copy link
Copy Markdown
Owner

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/ and keys/ 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.

  • New src/home.jsresolveHome(): LOTOR_HOME if set, else ~/.lotor.
  • Wired into all five entry points (MCP server, ingest, gate/view/approve CLIs). Library baseDir defaults stay '.' so unit tests are unaffected.
  • The morning-after integrity check read the chain pubkey from a hardcoded ./keys, so npm run receipts falsely 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.
  • New lotor_status MCP 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_action returns a setup hint when the gate is uninitialized.

Automatic session receipts

  • bin/hook-session-end.js: a Claude Code SessionEnd hook 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.
  • Parser fix: session.startedAt/endedAt now scan for the first and last entry with 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.

Docs

  • README gains an Install section: the two install decisions (where it runs, where receipts live) with implications, restart-to-activate, the key walkthrough, how to confirm it is live, and registering the hook.
  • .mcpb manifest declares user_config.lotor_home, so the Claude Desktop install asks the user where receipts live, with the local-disk implication in the field description.
  • KNOWN-LIMITS: session receipts are first-write-wins with no supersede on resume, and auto-capture is opt-in on the hook being registered.

Verification

  • 82 tests passing (was 66 at the start of this work).
  • Verified against a real Claude Code transcript: the receipt carries session id, model, version, non-null timestamps, turn/tool/failure counts, touched files, and outbound items. A second run dedupes and appends nothing.
  • End-to-end check confirms the MCP server and all CLIs share one store and write nothing into the working directory.

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.

githubscumand others added 7 commits July 21, 2026 15:29
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@githubscum