# Bookmark

Context snapshots for Claude Code. Restores session state across compactions, terminal closures, and restarts with zero manual steps.

Status: active

## Install

```bash
claude plugin marketplace add tyroneross/RossLabs-AI-Toolkit
claude plugin install bookmark@rosslabs-ai-toolkit
```

## The Problem

Claude Code sessions are ephemeral. Context evaporates three ways: compaction compresses the conversation and drops detail, closing the terminal starts a blank session, and switching between projects strands yesterday's decisions in a summary nobody reads. The recurring tax is re-explaining architecture, decisions, and open items every morning.

## What I Built

Bookmark captures Claude Code session context automatically and restores it when you return, so every new terminal starts where the last one stopped.

## Architecture

Bookmark splits into two tracks that run on the same four hook events, one deterministic and one LLM-authored, plus a restore path that reconciles them. A CLI (`@tyroneross/bookmark`, Commander.js) is the single entry point; a hand-rolled MCP server (JSON-RPC 2.0 over stdio, no SDK dependency) exposes the same five operations for MCP clients that aren't wired through hooks.

```mermaid
flowchart TD
    A["Stop / PreCompact hook fires"] --> B["bookmark stop/precompact (child process)"]
    B --> C["discoverTranscriptPath()<br/>~/.claude/projects/&lt;encoded-cwd&gt;/*.jsonl"]
    C --> D["parseTranscript()<br/>stream JSONL → typed entries"]
    D --> E["extractFilesAndTools()<br/>regex: Write/Edit calls, file-creating Bash"]
    E --> F[("snapshots/SNAP_*.json<br/>index.json · LATEST.md · trails/files.md")]
    F --> G[("~/.bookmark/registry.json<br/>cross-project index")]

    B --> H{".bookmark.context.md<br/>fresh? (&gt;200B, has ## header,<br/>newer than block marker)"}
    H -- stale --> I["hook returns block + systemMessage<br/>(Stop: blocks once, max 1 loop guard)<br/>(PreCompact: non-blocking nudge)"]
    I --> J["active Claude Code session<br/>writes bookmark.context.md<br/>via its own Write tool — no extra API call"]
    H -- fresh --> K[skip]

    L["SessionStart hook fires"] --> M["bookmark restore"]
    M --> N{"bookmark.context.md<br/>present + useful?"}
    N -- "home-scope pointer" --> O["follow points_to_canonical<br/>to the real repo bookmark"]
    N -- "repo-scope, ≥72h old" --> P["hard-block: return staleness<br/>guidance, not stale content"]
    N -- "repo-scope, fresh/24-72h" --> Q["emit as systemMessage<br/>(soft warning if 24-72h,<br/>repo_path mismatch warning if CWD differs)"]
    N -- missing --> R["fall back to LATEST.md<br/>(file-trail only)"]
    Q --> S["Claude Code injects into<br/>new session's context"]
    R --> S
    O --> S
```

**Tools & infra — which, why:**

| Component | Choice | Why |
|---|---|---|
| CLI framework | Commander.js | Small, dependency-light command parser for the `bookmark` binary's ~15 subcommands |
| Runtime | Node.js ≥20, TypeScript, compiled to `dist/` via `tsc` | npm-distributable; matches the Claude Code plugin/hook execution environment |
| Storage | Flat files — JSON snapshots, `.json` index, `.md` trail files under project-local `.bookmark/` | No database: single-user, single-project data, needs to be `.gitignore`d and human-readable, not queried |
| Cross-project index | `~/.bookmark/registry.json` (JSON, capped 500 entries) | Lets `bookmark list --all` and the home-scope pointer resolve "what was I last working on" across every project without a shared service |
| Session integration | Claude Code hooks (`Stop`, `PreCompact`, `SessionStart`, `UserPromptSubmit`), command-type, defined in `hooks/hooks.json` | The only mechanism Claude Code exposes for code to run at those lifecycle points and to feed content back into context via `systemMessage` |
| MCP server | Hand-rolled JSON-RPC 2.0 over stdio (`src/mcp/server.ts`), no MCP SDK dependency | Exposes `snapshot`/`restore`/`status`/`list`/`show` as callable tools for MCP clients, independent of the hook path; kept dependency-free to match the rest of the package |
| Distribution | npm package + Claude Code plugin marketplace (`tyroneross/RossLabs-AI-Toolkit`) | Two install paths: `npm install @tyroneross/bookmark` (hooks auto-configured via `postinstall`) or `claude plugin install bookmark@rosslabs-ai-toolkit` |
| Testing | Vitest | Matches the TypeScript/ESM toolchain; used for parser/identity unit tests |

**Models — which, where, why:** Bookmark itself makes zero LLM/API calls — extraction (step 3 below) is regex and JSON parsing, restore logic (step 6) is deterministic identity/staleness rules. The narrative content in `bookmark.context.md` (step 5) is written by whichever model is already running the *host* Claude Code session, at the moment that session is about to stop or compact — not a second inference call Bookmark initiates, and not billed separately. The README documents an optional "Smart Mode" that would call Claude Haiku directly (`--smart`, `ANTHROPIC_API_KEY`) for higher-quality extraction; ⚠️ needs confirmation — no `haiku`, `ANTHROPIC_API_KEY`, or Anthropic SDK reference exists anywhere in `src/`, and `package.json` lists no Anthropic dependency, so this mode does not appear implemented in the current source.

⚠️ The README's "Adaptive Thresholds" table (snapshot-at-20%/30%/40%/50%-context-remaining, scaling with compaction count) describes logic that exists in `src/threshold/adaptive.ts` but is not called anywhere outside its own module — the `UserPromptSubmit` handler (`bookmark check`) only runs `checkTimeInterval()`, a pure elapsed-time comparison; the code comment states "token estimation removed (can't know actual context usage)." The live trigger is time-based only (default every 5 minutes, not the README's 20 — configurable via `bookmark config --interval` or `BOOKMARK_INTERVAL`).

## How it works

1. **Hook fires.** `Stop` (session end), `PreCompact` (before transcript compression), or `UserPromptSubmit` (every user message, async) invokes the `bookmark` CLI as a plain command-type hook — a child process, no stdin piping for `stop`/`precompact`. `SessionStart` invokes `bookmark restore`.
2. **Transcript discovery.** `discoverTranscriptPath()` locates the session's JSONL under `~/.claude/projects/<encoded-cwd>/`, preferring the file matching `CLAUDE_SESSION_ID`, else the most recent substantial (>10KB) `.jsonl`.
3. **Deterministic extraction.** `parseTranscript()` stream-parses the JSONL into typed entries (user/assistant/tool_use/tool_result), pulling `tool_use`/`tool_result` blocks out of nested `message.content` arrays. `extractFilesAndTools()` then walks those entries for `Write`/`Edit` calls and file-creating Bash patterns (`mkdir`, `touch`, `mv`, `cp`, `cat >`, `tee`) to build a file-activity list (path, operation, lines changed, structural hints like `+fn foo`/`+class Bar` from diffing old/new strings against a small regex table) and a tool-usage histogram. This is the *only* content the deterministic path extracts — as of v0.3 the extractor's own comments state decision/intent/status/error extraction was removed because "regex can't do semantic extraction reliably."
4. **Deterministic write.** `captureSnapshot()` writes `.bookmark/snapshots/SNAP_<timestamp>.json`, updates `.bookmark/index.json` (capped at 50 active entries), regenerates `.bookmark/LATEST.md` (files + tools only, via `compressToMarkdown`), merges into `.bookmark/trails/files.md` (a running per-file trail across snapshots), and appends to `~/.bookmark/registry.json`, a cross-project index capped at 500 entries. Snapshots with zero files changed are skipped (except manual triggers) to avoid empty-snapshot accumulation.
5. **LLM-authored narrative.** The same `stop`/`precompact` invocation checks `.bookmark/bookmark.context.md` for freshness (>200 bytes, contains a markdown `## ` header, newer than any prior block marker). If stale, `stop` returns `{"decision":"block","systemMessage":"..."}` telling the *already-running* Claude Code session to write task/progress/decisions/branch/files to `bookmark.context.md` via its own `Write` tool — capped at one block per session via a `.stop-requested` marker so it can't loop. `precompact` does the equivalent non-blockingly (systemMessage nudge only, 5-minute staleness window, never blocks compaction). No separate model call happens here — it's the same inference pass the user is already paying for, redirected to also produce a summary.
6. **Restore.** `SessionStart` runs `bookmark restore`, which reads `bookmark.context.md`, parses an optional `BOOKMARK_IDENTITY` header (`scope`, `project`, `repo_path`, `branch`, `head`). A `scope: home` pointer (written at `~/.bookmark/bookmark.context.md` when a session ends) is followed to the real repo-scoped file so a session started from `~` still restores the right project. A `repo_path` mismatch against the current CWD prefixes a warning rather than silently injecting the wrong project's context. Content ≥72h old is hard-blocked — the restore returns staleness guidance instead of the stale content, on the reasoning that a confident wrong start is worse than no restore; 24-72h gets a soft inline warning instead.
7. **Fallback.** If `bookmark.context.md` is missing or fails a content-usefulness check, restore falls back to `LATEST.md` — the deterministic file-trail snapshot from step 4 — so a session that never got a narrative write still recovers *something*.
8. **Injection.** The result is emitted as a hook `systemMessage`, which Claude Code injects into the new session's context automatically — no manual paste, no separate restore command.

## Why This Design

Running as an external process keeps the deterministic path (file/tool tracking, restore logic) at zero tokens. The narrative path deliberately does *not* try to out-regex an LLM for decisions and intent — it hands that job to the model already running the session, at the moment it's already about to stop or compact, instead of adding a second inference call. The hard 72-hour staleness block exists because a soft warning on old content reads as noise; Claude tends to treat a stale summary as current unless restoration is refused outright. The result: install it, forget about it, pick up tomorrow.

## Tech stack

TypeScript on Node.js ≥20, compiled with `tsc`, distributed as an npm package and a Claude Code plugin. Commander.js drives the CLI. Claude Code's four command-type hooks (`Stop`, `PreCompact`, `SessionStart`, `UserPromptSubmit`) are the only integration surface; a hand-rolled MCP server (JSON-RPC 2.0 over stdio, no SDK) exposes the same operations to other MCP clients. Storage is flat files only — JSON snapshots and index, Markdown trail/context files — under project-local `.bookmark/` plus a capped cross-project registry at `~/.bookmark/registry.json`; there is no database, vector store, or queue anywhere in the system. Vitest covers the parser and identity modules.

## Results

Worked example: a long session approaches the compaction threshold mid-task. `PreCompact` fires first, deterministically snapshots which files changed and what tools ran, and nudges Claude to write a `bookmark.context.md` narrative if the existing one is more than 5 minutes stale. If the terminal is closed instead, `Stop` does the same file/tool capture and, if `bookmark.context.md` hasn't been touched in the last 2 minutes, blocks exit once to force the write. Either path leaves both a file-trail snapshot and (if the write succeeded) a Claude-authored summary on disk. The next time a terminal opens in that project, `SessionStart` restores the most recent usable one into context automatically — task, decisions, and open items from the narrative file if present and not stale, file/tool activity from `LATEST.md` if not — so the new session starts from prior state instead of a blank slate.

Because the narrative write happens inside the same inference pass that's already running (the session about to stop or compact writes its own summary), and the file-trail extraction is pure regex/JSON parsing, Bookmark itself adds no LLM cost. The only tokens that enter a new session's context window are whatever the restored `systemMessage` contains.

Restore latency, snapshot size, and cross-session summary accuracy: ⚠️ no benchmark yet.

## Links

- [HTML project page](https://rosslabs.ai/projects/bookmark/)
- [Live app](https://www.npmjs.com/package/@tyroneross/bookmark)
- [Source code](https://github.com/tyroneross/bookmark)
