- Python 72.1%
- Shell 27.9%
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). |
||
|---|---|---|
| .env.example | ||
| .gitignore | ||
| _loadenv.sh | ||
| backfill.py | ||
| claude-recent | ||
| CLAUDE.md | ||
| install.sh | ||
| log-event.sh | ||
| preview-session.sh | ||
| README.md | ||
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:
- Creates
.envfrom.env.exampleif missing - Creates the data dir (per
$CLAUDE_SESSIONS_DIR, env wins over.env) - Marks the scripts executable
- Backs up
~/.claude/settings.jsonand merges the three hooks (de-duping any priorlog-event.shentries) - Symlinks
claude-recentinto~/.local/bin/ - 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:
~/.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.~/.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.~/.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-runningbackfill.pylater picks up summaries for past sessions. - Per-host hooks and history. You must run
install.shANDbackfill.pyon 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. Runinstall.shagain and start a freshclaudesession.- 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, checkflockis on$PATH(util-linuxpackage on Debian/Ubuntu) and that the shared filesystem honors advisory locks. - Picker shows nothing / exits silently:
fzfis missing (apt install fzf) or yourfzfis older than 0.36 (--with-nth=2..syntax requires a recent build).