No description
  • Python 72.1%
  • Shell 27.9%
Find a file
Ricardo 054ad09fe2 docs: clarify single-host use and shared-filesystem requirements
Document the single-machine setup as a first-class path (point
CLAUDE_SESSIONS_DIR at a local dir), explain the origin column and its
hostname fallback, and spell out which filesystems give reliable flock
(local ext4, including multi-WSL2 under one kernel) vs the network-FS
caveat (NFS/sshfs/9p).
2026-06-27 16:54:49 +02:00
.env.example Add .env-based config layer 2026-05-04 08:56:15 +02:00
.gitignore Add .env-based config layer 2026-05-04 08:56:15 +02:00
_loadenv.sh Add .env-based config layer 2026-05-04 08:56:15 +02:00
backfill.py Add .env-based config layer 2026-05-04 08:56:15 +02:00
claude-recent Add .env-based config layer 2026-05-04 08:56:15 +02:00
CLAUDE.md docs: clarify single-host use and shared-filesystem requirements 2026-06-27 16:54:49 +02:00
install.sh chore: mark install.sh executable 2026-06-27 16:34:51 +02:00
log-event.sh fix: fall back to hostname for distro on non-WSL hosts 2026-06-27 16:34:14 +02:00
preview-session.sh Add .env-based config layer 2026-05-04 08:56:15 +02:00
README.md docs: clarify single-host use and shared-filesystem requirements 2026-06-27 16:54:49 +02:00

claude-recent

Cross-machine session tracker for Claude Code. Index your sessions to a shared folder, browse them with an fzf TUI, and resume any of them from anywhere.

When this is useful

Anywhere you run Claude Code on more than one machine (or distro, or container) that all share a writable directory:

  • Two WSL distros sharing /mnt/wsl/dev/ — the original use case. WSL crashes are common and tab loss costs context.
  • Local laptop + a remote dev box on a sshfs/NFS mount.
  • Multiple containers writing to a shared volume.
  • A single machine where you just want a unified, persistent log of every Claude session — even one host benefits from the searchable history.

What you stop losing:

  • which directory each session was running from
  • the session ID needed for claude --resume
  • which of the last N sessions was the one you actually care about

How it works

A tiny hook fires on SessionStart, UserPromptSubmit, and Stop, appending one JSON line per event to a shared index file. All participating Claude installs append to the same index (with flock for safety). A query CLI aggregates the events per session and prints recent sessions with a copy-pasteable resume command, or opens an interactive picker.

The default shared path is /mnt/wsl/dev/.claude-sessions/ (the WSL case). Override with $CLAUDE_SESSIONS_DIR for any other shared directory.

Layout

<repo>/                          # the code, somewhere both machines can read
├── log-event.sh                  # hook handler (writes to the index)
├── claude-recent                 # query CLI / fzf picker
├── preview-session.sh            # fzf preview pane (used by --pick)
├── backfill.py                   # one-shot historical seeder
├── install.sh                    # idempotent installer
├── _loadenv.sh                   # .env loader (sourced by bash scripts)
├── .env.example                  # config template — copy to .env to edit
├── README.md
└── CLAUDE.md                     # install instructions for an AI agent

$CLAUDE_SESSIONS_DIR/             # runtime data — must be writable from
├── index.jsonl                   # every host you want to track
└── .lock

Configuration

install.sh creates a local .env from .env.example if missing. Edit .env to change defaults — point the data dir at an NFS mount, an sshfs path, a container volume, or wherever your hosts have shared write access:

# .env
CLAUDE_SESSIONS_DIR=/mnt/share/claude-sessions

Precedence (highest first): real environment variables → .env file → hardcoded fallback (/mnt/wsl/dev/.claude-sessions/). So you can override per-shell with export CLAUDE_SESSIONS_DIR=... without touching the file.

.env is gitignored — it's per-host config, not source.

Single machine (just one Claude install)

You don't need a shared drive or a second host — claude-recent works just as well as a plain, persistent session log on one machine. Point the data dir at any local path:

# .env
CLAUDE_SESSIONS_DIR=/home/you/.claude-recent

Then run install.sh + backfill.py once, and claude-recent gives you a searchable history of every session on this host. Nothing else changes — the multi-host machinery (flock, the shared index) is simply a no-op when there's only one writer. The bracketed origin column (see What gets logged) just shows your hostname instead of a WSL distro name.

Install

On each machine / distro / container you want to track, clone or otherwise make this repo readable, then:

bash <repo>/install.sh
python3 <repo>/backfill.py    # optional but recommended

The installer:

  1. Creates .env from .env.example if missing
  2. Creates the data dir (per $CLAUDE_SESSIONS_DIR, env wins over .env)
  3. Marks the scripts executable
  4. Backs up ~/.claude/settings.json and merges the three hooks (de-duping any prior log-event.sh entries)
  5. Symlinks claude-recent into ~/.local/bin/
  6. Smoke-tests the hook end-to-end and removes the test record

backfill.py seeds the index with sessions that pre-date the hook, reading three sources in order:

  1. ~/.claude/projects/*/sessions-index.json — when present, has the friendly auto-generated summary Claude writes per session ("Rybbit deployment: Docker migration, ..."), the first prompt, and the canonical cwd. Only a fraction of projects have one.
  2. ~/.claude/projects/*/<sessionId>.jsonl — the raw transcripts. Walked directly to extract cwd and the first non-sidechain, non-meta user prompt. This is where the bulk of history actually lives.
  3. ~/.claude/sessions.db — fallback for old sessions whose JSONL is no longer on disk (e.g. from a prior WSL install). Yields cwd + last_activity + model, but no title.

It filters out sub-agent transcripts (<project>/<sessionId>/subagents/agent-*.jsonl), sidechain entries, and sessions whose "first prompt" is actually a system meta-prompt template (<command-name>, <system-reminder>, <local-command-caveat>, summarizer wrappers, etc.).

Idempotent: re-running only adds session IDs that aren't already in the index.

Both scripts are idempotent — re-run either any time.

Usage

claude-recent              # last 20 sessions, sorted by recency
claude-recent 50           # last 50
claude-recent --pick       # interactive picker (fzf)
claude-recent --here       # only sessions whose cwd is under $PWD
claude-recent --distro Ubuntu
claude-recent --days 7     # only sessions active in the last 7 days
claude-recent --raw        # JSON, one session per line (for scripting)

Plain list

   3m ago  [Ubuntu  ]  3f5b37e7  ( 12 ev)  fix the helm chart for tyk-cleanup prd
  cd /mnt/wsl/dev/openshift-central/tyk && claude --resume 3f5b37e7-...

The second line is copy-pasteable.

The bracketed column ([Ubuntu ]) is the session's origin host: the WSL distro name ($WSL_DISTRO_NAME) when set, otherwise the machine's hostname. It exists to tell apart multiple WSL distros that share a single hostname — when you spot the session you want, it tells you which distro to run the resume command in (transcripts live in each host's own ~/.claude/, so --resume only works on the host that created the session). On a single non-WSL machine it's simply your hostname.

Interactive picker

eval "$(claude-recent --pick)"

Opens an fzf picker. Type to filter (matches title or cwd), arrow keys to navigate, Enter to print the resume command, Esc to cancel. The right pane previews session details (summary, first prompt, model, event timeline, resume command).

If you don't want to eval, just run claude-recent --pick and copy the printed command yourself.

Requires fzf (apt install fzf).

What gets logged

Hook (live) records per event: ts, distro, host, user, ppid, event, session_id, cwd, source, transcript, prompt(first 200 chars). Prompts are truncated; only the first prompt of a session becomes the displayed title.

Backfill records add: summary (when sourced from sessions-index.json), model, message_count. The query CLI prefers summary over prompt when showing a title, so freshly-logged sessions get nicer names later when Claude writes a summary and you re-run backfill.py.

Limitations

  • Friendly titles come from sessions-index.json (Claude auto-generates a summary per session, e.g. "Rybbit deployment: Docker migration, ..."). Live sessions logged via the hook only show the first user prompt — the summary is added by Claude after-the-fact. Re-running backfill.py later picks up summaries for past sessions.
  • Per-host hooks and history. You must run install.sh AND backfill.py on every host that should contribute to the index. The shared index file unifies them, but each host's ~/.claude/ is separate.
  • Append-only log. No automatic rotation. The file is small (~200 bytes per event) but truncate it manually if it ever bothers you.
  • DB sessions without local JSONL (e.g. from a prior WSL install) get backfilled with cwd + last_activity but no title.

Uninstall

# Remove hooks (manual)
$EDITOR ~/.claude/settings.json   # delete the three log-event.sh blocks
rm ~/.local/bin/claude-recent
# Optionally:
rm -rf /mnt/wsl/dev/.claude-sessions

Troubleshooting

  • Empty list after starting a session: the current shell session is logged only after its next SessionStart/prompt. Hooks don't fire retroactively.
  • No session index yet: hook hasn't fired. Run install.sh again and start a fresh claude session.
  • Concurrent writers from multiple hosts: handled with flock. This is fully reliable on a real local filesystem — including an ext4 disk mounted into several WSL2 distros, because WSL2 runs all distros under one shared kernel that arbitrates the locks. Advisory locks are historically unreliable over network filesystems (NFS, sshfs) and the Windows-mediated \\wsl$ / 9p mounts, so prefer a genuine block device for the shared dir. If you see garbled lines, check flock is on $PATH (util-linux package on Debian/Ubuntu) and that the shared filesystem honors advisory locks.
  • Picker shows nothing / exits silently: fzf is missing (apt install fzf) or your fzf is older than 0.36 (--with-nth=2.. syntax requires a recent build).