agentleFS
johan-lindahljohan-lindahl/session-explorer/CLAUDE.md

session-explorer

CLAUDE.md0 starsChanged 5 months agoMITView on GitHub

Implemented and released, installable from the Claude Code marketplace. The hook, manifest, index core, Textual TUI, --gc retention, uninstall, launchers, the live-session indicator, the split-pane tmux interaction layer (explorer left / active session docked right, F9 switch, F12 fullscreen), new-session creation (c), and CI are all shipped. SPEC.md is the authoritative source for architecture, data model, TUI behavior, install layout, and edge-case decisions — read it before proposing or writing any code. If a change would contradict the spec, update…

  • Installs packages
  • Commits and pushes

The file

We split this CLAUDE.md into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All milestones (M1–M7) are shipped as of v1.5.0.

    line 109

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Status

Implemented and released, installable from the Claude Code marketplace. The hook, manifest, index core, Textual TUI, --gc retention, uninstall, launchers, the live-session indicator, the split-pane tmux interaction layer (explorer left / active session docked right, F9 switch, F12 fullscreen), new-session creation (c), and CI are all shipped. SPEC.md is the authoritative source for architecture, data model, TUI behavior, install layout, and edge-case decisions — read it before proposing or writing any code. If a change would contradict the spec, update the spec in the same change rather than silently diverging.

What this project is

session-explorer — a Claude Code plugin that turns the JSONL transcripts under ~/.claude/projects/ into a file-explorer-style tree. Single entry point:

  • /session-explorer:open spawns a new terminal window running a Textual TUI. (Plugin commands are namespaced as <plugin>:<command>; the prefix is unavoidable.)
  • The TUI is the entire UX: browse, rename, move between folders, delete, edit notes, resume. No other slash commands.
  • Index sidecar at ~/.claude/session-explorer-index.json caches per-session metadata. User-created empty folders and per-project folder paths live in the separate folder store at ~/.claude/session-explorer-folders.json.
  • SessionStart hook records new sessions and refreshes the cache. It never modifies settings.json; retention is opt-in (see below).

Load-bearing design decisions

These are the constraints to preserve — violating any breaks the spec's contract:

  • The user's session name is the only "tag". The plugin does not maintain a parallel tag field. /rename (or claude -n) is the single source of truth — both write a custom-title event to the JSONL.
  • A live session re-emits its custom-title every turn, so "last-title-wins" alone reverts external renames. After the explorer renames a running session, Claude's next re-emit appends the OLD title as the JSONL's last line. index.set_name therefore records superseded titles in name_shadows[], and record_session ignores a shadowed last-title (keeping the user's name_cached). A new, unshadowed title is still adopted, so an in-session /rename still flows through. Don't restore naïve last-wins in record_session, and route all explorer renames through index.set_name (not a bare name_cached mutate). See jsonl.all_custom_titles.
  • ai-title is NOT a name. Claude emits ai-title events automatically as it summarises an active session; those auto-generated titles are intentionally ignored by jsonl.session_name(). Only custom-title counts. Don't reintroduce the ai-title fallback.
  • "Kept" is implicit — a session is kept iff it has a (custom-title) name. Don't reintroduce a separate kept flag.
  • Unnamed sessions are hidden from the TUI by default. Press u to surface them for renaming or deletion. The index still tracks them (so --gc can expire stubs on schedule); only the default view filters them out. Don't conflate "hidden" with "deleted" — these are orthogonal.
  • Worktree sessions group under the parent repo. A cwd of <repo>/.claude/worktrees/<name> derives project_label from <repo>, not the worktree leaf — otherwise every worktree becomes its own top-level "project". project_path keeps the worktree path so resume chdir's into the correct working tree. See index._project_label.
  • Worktree cleanup is non-destructive and explorer-owned. Removal goes through worktree.remove() (bin/_pkg/worktree.py) using git worktree remove without --force (git refuses dirty/untracked trees — never force it) and never deletes the worktree-<name> branch, so a removed worktree is just a "dead" one that _recreate_worktree rebuilds on resume. Exception (v1.18.0): permanent session deletion has nothing left to rebuild, so worktree.purge() (a separate primitive) removes the dir and safe-deletes the branch with git branch -d (merged-only — an unmerged branch or a dirty tree is kept, never -D/--force). worktree.remove() itself is unchanged and still keeps the branch for the w/gc reclaim paths. delete_session (d) and gc.collect_garbage (--gc) both call purge and both drop the session's summary sidecar entry. The explorer owns this because Claude only offers native cleanup from the -w-creating process, and our retention cleanupPeriodDays = 36500 disables Claude's age-based sweep. Three triggers share the primitive: w (action_remove_worktree), the docked-exit offer in _poll_live (_maybe_offer_worktree_cleanup, once-per-sid via _offered_cleanup), and gc.collect_worktrees (idle >14d, clean, not live). collect_worktrees does NOT mutate the index and may reclaim directories of kept sessions — transcript + branch survive, so it's safe. The worktree git/FS primitives live in worktree.py so gc.py needn't import Textual.
  • Slash splits folder path from display name. team/planning/sprint14 → folder path team/planning, display sprint14. Multiple / create nested folders. Dashes are literal characters with no special meaning. Empty segments are dropped.
  • Folder structure lives in ~/.claude/session-explorer-folders.json, scoped per-project. Sessions named with / auto-add their path to the store on indexing. Pre-created empty folders live there too. The session index file no longer carries a folders[] field; a one-shot v1→v2 migration moves any legacy entries under a synthetic (unfiled) project.
  • Don't move or rewrite native JSONLs. Sessions stay where Claude Code wrote them so /resume keeps working. The only legitimate write to a JSONL is appending a rename event in the same shape Claude's own /rename writes.
  • Retention is opt-in (modifying cleanupPeriodDays is the review-sensitive bit). Neither the hook nor install.sh touches settings.json. The TUI asks on first launch (tui.on_mount → retention.enable/decline); only on "yes" is the prior cleanupPeriodDays backed up to ~/.claude/.session-explorer.backup and set to 36500. The backup's existence is the "retention enabled" signal; a .session-explorer.retention-declined marker records a "no" so the prompt isn't re-shown.
  • --gc only runs once retention is enabled. The plugin's --gc does retention work, gated on name_cached IS NULL; the hook's once-daily auto-trigger is additionally gated on the backup file existing. uninstall restores cleanupPeriodDays from the backup.
  • Concurrent index writes (two Claude sessions starting at once). Use flock + temp-file-rename for every write.
  • --gc skips live sessions. Skip any JSONL with an active flock or mtime within 60s.
  • Hooks never block startup. SessionStart logs failures to ~/.claude/session-explorer.log and exits 0.
  • The explorer's own death must be visible and self-healing. _run_app logs every crash traceback to ~/.claude/session-explorer.log — including the ones Textual swallows: an exception in a message handler / set_interval/set_timer callback / worker never escapes app.run(), Textual just sets return_code = 1 and returns normally, so _run_app inspects return_code/_exception after a clean return too, and run() propagates that non-zero code instead of running the execvp hand-off (v1.19.3 — returning 0 there closed the remain-on-exit=failed pane, wiped the on-screen traceback and ceded the window to the docked claude, leaving a crash with no evidence anywhere). Don't make run() return 0 on a crash; the TUI marks its own pane remain-on-exit failed at mount (crash → dead pane with traceback on screen, never a claude-swallowed window; clean exit still closes); the launcher respawns a dead explorer pane before new-session -A. The periodic thread workers (live-meta, usage) call guarded _*_tick bodies — @work defaults to exit_on_error=True and call_from_thread re-raises UI-side exceptions in the worker, so an unguarded periodic tick failure kills the entire app. Don't add a new @work worker without the same guard, and don't "simplify" the tick wrappers away.
  • User text is DATA, never markup — escape it at every render site. Session names, notes, summaries, first prompts, branches, paths, dialog strings and search terms all reach the screen through markup templates, and Textual emits a [ run it can't tokenize as literal text — which swallows the ] of the next real tag and leaves the template's trailing [/]s with nothing to close (MarkupError out of Static.update, inside an interval callback → the app dies; that is how feature/51010-[ADMIN] … killed the explorer — fixed v1.19.4). Route every user-derived value through markup.escape (bin/_pkg/markup.py, Textual-free so search.py can share it). It escapes every [: don't "simplify" it to rich.markup.escape/textual.markup.escape, which only cover [a-z#/@]-initial tags and let [ADMIN] through — Rich ignores that too, but Textual parses it as a tag, and _preview_text is rendered by both engines (Static.update → Textual, _render_live_preview → Text.from_markup). Escape LAST, after truncation and column padding: the backslashes render as zero cells, so widths computed on the raw text stay aligned, and truncating escaped text can cut a \[ in half.
  • A crashed explorer is not a claude-swallowed impostor. v1.19.3's remain-on-exit=failed leaves a dead TUI pane in the explorer window so the traceback stays on screen and the launcher respawns it in place — but that window has no live TUI pane, which is precisely heal_explorer_impostors's test for a window a docked claude took over. Renaming it aside deletes the explorer:explorer target the respawn needs and strands the docked claude under a name the tree can't map ("this session is already running in another terminal"). heal_explorer_impostors therefore skips any window containing a dead pane — only the TUI pane can be dead, since claude panes keep the default remain-on-exit=off — and derives the sid by scanning every pane, never panes[0] (after a crash that's the dead TUI, which is what produced orphan-<pid> window names). Don't reintroduce a live-TUI-only test here.
  • TUI runs in a separate terminal, not inside Claude Code. Claude holds the terminal in raw mode; an interactive TUI must have its own TTY. Slash command spawns it via OS-detected launcher (osascript on macOS; $TERMINAL / x-terminal-emulator / known emulators on Linux).
  • One Python dep: vendored Textual. Bundled under bin/_pkg/_vendor/. No pip install runs on either install path. Don't add other deps casually.
  • Don't sum input_tokens / output_tokens from the JSONL for context-size stats. Those are streaming-time estimates and have been observed to be order-of-magnitude wrong. Use cache_read_input_tokens from the latest assistant message; fall back to bytes / 4 when caching wasn't active. UI labels the value with ~ to set expectations.
  • No in-place /compact. v1 surfaces context size only; compaction stays a manual /compact inside a resumed Claude session. Don't reintroduce SDK-driven compaction without revisiting the spec.
  • Session summaries shell out to claude -p, guarded so they leave no trace (v1.18.0). summarize.run() (bin/_pkg/summarize.py) spawns claude -p (Haiku default) with SESSION_EXPLORER_SUMMARIZER=1 and SESSION_EXPLORER_PROBE=1 in the env, so hooks/session-start.sh bails at its early-exit guard and the summariser session records no index row / current-pointer / gc. Summaries live in a ~/.claude/session-explorer-summaries.json sidecar (summary.py, flock + temp-rename like the other stores) — never in the index (it's rewritten every ~2s during live refresh). The auto-on-exit worker (_maybe_summarize → _summarize_worker) is a guarded @work (log-and-skip on failure, same rule as _live_meta_tick — an unguarded worker exception exits the app). _populate merges summary text into the in-memory row dicts so _matches (the / filter) and _preview_text see it without touching disk. Auto-summarise defaults off and is a toggle (marker .session-explorer.summaries-auto), not a permanent decision — a first-run prompt (marker .session-explorer.summaries-prompted, and env SESSION_EXPLORER_SUMMARY_NO_PROMPT suppresses it in CI/tests) introduces it, and the Settings screen flips it forever after. Only custom-title-named sessions with ≥SUMMARY_MIN_MSGS (8) are auto-summarised. u works on a live session too (v1.19.0) — build_digest/jsonl._iter_messages tolerate a mid-write transcript (partial last line is skipped), so there's no "stop it first" refusal; a summary made while the session is live is stored with provisional: true and is repeatable (press u again for a fresh snapshot). On exit, _maybe_summarize(ended) iterates the just-stopped sids and regenerates any that carry a provisional summary — to a final one, bypassing the auto-toggle and the length threshold (the user explicitly asked for it) — in addition to the docked session when auto-summaries is enabled. _start_summarize/_summarize_worker/_summarize_tick thread a provisional flag into the sidecar entry; _refresh_preview merges the sidecar summary + provisional + summarizing flags before the live-preview branch so a running session's preview shows its latest snapshot, the "live snapshot — refreshes on exit" tag (_summary_header), and the "⏳ Summarising…" indicator.
  • The preview pane is a bottom pane, and Settings is the home for persisted prefs (v1.18.0). compose() stacks the preview inside the treepane Vertical (between tree and queues), not in a right-side Horizontal — the explorer is the left tmux pane, so a right-side preview was squeezed against a docked session. Space still toggles it. The SettingsScreen (,) is the single surface for persisted toggles — auto-summaries, retention (with a configurable period in ui_state.retention_days, read by collect_garbage when --retention-days is omitted), usage bar (mirrors g), queues pane (mirrors q) — plus a status/set-up-only tmux row (never a disable; re-surfaces the install offer after a decline). Row rendering/activation live on the app (_settings_rows/_settings_activate, unit-tested); retention.disable() restores the backed-up cleanupPeriodDays. Keep u/, in sync across BINDINGS, check_action, and _help_text.
  • Resume is non-destructive when tmux-hosted. The explorer is the left pane of the explorer window and stays alive; the active session docks as the right pane (join-pane), while inactive sessions keep running as background windows. F9 switches focus between the two panes (also mouse-click); F12 zooms the focused pane fullscreen. Without tmux it falls back to execvp. Don't reintroduce unconditional exit-on-resume, and don't reintroduce the old window-flipping/window-tab model (the explorer tree is the only session switcher).
  • tmux is an optional, consented dependency. Detect + offer install (declined-marker at ~/.claude/.session-explorer.tmux-declined), never bundle a binary, never silent-sudo. The dedicated -L session-explorer server never touches the user's tmux.
  • Snapshots are read-only. capture-pane for our tmux windows, transcript-tail otherwise. No embedded interactive terminal widget.
  • Full-text search (f) is a live scan, not an index (v1.19.0). search.py (Textual-free, like root_guard.py/gc.py) reads the current project's JSONL bodies at search time — user+assistant message text only (same extraction shape as summary.build_digest), tool output/thinking/system dropped. There is no fourth sidecar; don't add one and don't fold body search into _matches (the / filter stays metadata-only — name/notes/first-prompt/summary). Scope is current-project (grouped by tree_model.session_root, so worktrees collapse in), not cross-project. The scan runs in a guarded worker (SearchScreen._run_search logs-and-skips — an unguarded worker exception exits the app, same rule as _summarize_tick). f selection is non-destructive (moves the tree cursor via _pending_select_sid, never resumes). Rename SearchScreen's result-render method away from _render — that name is a reserved Textual widget hook. Keep f in sync across BINDINGS, check_action (modal-up guard + /-filter-focus guard), and _help_text.
  • Sessions persist by default; only an explicit shut-down kills the server (v1.15.0, reverses "Option C"). The generated tmux config has no client-detached kill hook, so detaching the client by any means (red-button/Cmd+W, crash, or the deliberate [b] leave-running quit) leaves the server and all background sessions running; the next /open reattaches via new-session -A. Only x → [s] ("shut down all") runs kill-server. The persist-flag mechanism (marker file + set/clear/persist_flag_set) was removed. Don't reintroduce a detach-time kill.
  • A new session's launch failure is captured, not silent. New-session windows run exec claude … 2> a per-sid errfile; ~1.5s after launch _check_launch (tui.py) verifies the session is alive and, on death (typically claude -w failing to create its worktree), surfaces+logs the captured stderr and stamps the index row's last_launch_error (shown in the preview). The docked-pane liveness arm verifies the pane actually exists — a claude that docks then dies within the delay window leaves _docked_sid as a phantom, which must read as dead (and be cleared), not alive. A transcript-less stub is started fresh on Enter via --session-id (_start_stub_fresh), never --resume. record_session clears last_launch_error once a transcript appears.
  • transcript_path in the index can be dangling — check the disk before writing/resuming. The hook records the path at SessionStart, but claude creates the file (and its project dir) only on the first message. Rename/move/folder-cascade append the JSONL event only when the file exists (tui._transcript_on_disk); otherwise the rename is index-only, and index.set_name shadows the replaced name_cached directly so claude's first write (re-emitting its -n title) can't revert it. Enter's stub test is likewise "no messages AND no transcript file on disk" (never --resume a dangling path), but a running transcript-less session docks instead of restarting. Don't reintroduce O_CREAT-ing claude's transcript before claude does — the missing project dir made it crash the TUI mid-callback, and the dead pane let the docked claude swallow the explorer window.
  • Claude Code RELOCATES a worktree session's transcript when the worktree is gone — the explorer must follow it (v1.19.1). As of Claude Code ~2.1.x, resuming (via native /resume) a session whose worktree was removed moves the JSONL out of ~/.claude/projects/<encoded-worktree-cwd>/ into the parent repo's project dir and writes a {"type":"relocated","relocatedCwd":...} line. This breaks the old "branch + transcript kept; resume rebuilds it" assumption: the index row's transcript_path/project_path dangle and the session drops out of the tree though its data is intact. index.reconcile_relocated re-links any dangling row to the moved <sid>.jsonl at its true cwd — read cwd via jsonl.effective_cwd (last relocatedCwd, NOT the dead worktree cwd that session_cwd returns), and never decode the project-dir name (magento-os → magento/os is wrong). It merges onto the existing row (notes/name_shadows/created_at survive), unlike an F5 refresh_all prune which deletes the row first. Runs from on_mount, a RECONCILE_POLL_INTERVAL guarded worker (_reconcile_relocated_tick, same log-and-skip rule as _live_meta_tick), and at the top of reindex (so the prune keeps the healed row). Only scans disk when a tracked row is actually dangling; skips live sids and transcript-less stubs.
  • A relocated worktree session must RE-ISOLATE on resume, not run in the root (v1.19.2). A relocated session's project_path is the shared root, so a plain claude --resume would run it in the root — where root_guard._is_root_location classifies it as a guard-exempt "root owner", so it touches the shared installed root without a lease and its dirt makes every worktree agent's lease acquisition refuse (blocks the queue). Fix: reconcile_relocated also stores worktree_leaf (via jsonl.worktree_origin, recovered from the transcript's pre-relocation /.claude/worktrees/<leaf> cwds — survives relocation). action_resume/_exit_to_resume detect a relocated worktree-born row (_relocated_worktree_leaf: worktree_leaf set AND project_path no longer under worktree.MARKER) and resume via claude -w <leaf> --resume=<sid> launched from the root (so --resume finds the root-key transcript; -w rebuilds the worktree on its kept worktree-<leaf> branch and re-enters it). Empirically proven (real claude 2.1.199, throwaway repo): -w --resume recreates the worktree, the session's tool cwd AND the SessionStart-recorded cwd both become the worktree → root_guard treats it as a worktree session (must queue-run). Threaded through tmux.build_start_window(worktree=), _dock(worktree=), _resume_argv(worktree=). The explorer NEVER moves a JSONL — Claude owns the relocation. Don't reintroduce a plain in-root resume for a worktree_leaf row.
  • Shared-resource queue keys by git-common-dir, not project_root. The Phase-1 lease engine (queue-run/queue-status/queue-cancel, bin/_pkg/queue_*.py + project_id.py + qsync.py + exclusive.py + probes.py) identifies a project by project_id.project_id(cwd) (a hash of git rev-parse --git-common-dir), so every worktree of a repo collapses to one queue identity — index.project_root() (a /.claude/worktrees/ string-strip) is NOT used for queues. The queue is the set of ticket files; the holder is the lowest-numbered ticket whose owner still holds its lifetime fcntl.flock (no daemon, crash-safe). The sync acquire strategy runs rsync -a --delete with anchored --filter=exclude rules and refuses the first sandbox transition until untracked/ignored would-delete paths are classified into protect/allow_delete. Never reintroduce --delete-excluded or rsync P/protect rules. See SPEC.md → "Shared-resource lease engine".
  • Phase-3 enforcement is location-based (fail-closed semantics, fail-open plumbing). For a Claude session in a worktree, the shared installed root is unreachable through tools (Bash|Edit|Write|NotebookEdit) except via a single session-explorer queue-* command. Decision logic lives in bin/_pkg/root_guard.py (pure, Textual-free, unit-tested); hooks/pre-tool-use.sh → queue-guard is the hook shell, unchanged. Plumbing fails open (a broken hook emits nothing and exits 0); within working plumbing a root mention from a worktree session is denied with the exact queue-run rewrite. The matcher widens from Bash to Bash|Edit|Write|NotebookEdit in .claude-plugin/plugin.json and install.sh, torn down in uninstall.py (_HOOK_MARKERS/_HOOK_EVENTS) — keep all three in sync. SessionStart awareness (queue_awareness._render_context) shrinks to a ≤6-line usage hint. Deleted: guard_match.py ({exe, sub} guard vocabulary), queue_detect.py (mtime out-of-lease toast). Per-project setup is a parameterless s toggle (v1.17.1): no dialog — action_resource_setup pushes a ConfirmScreen that either stops sharing (current overlay resource) or enables/migrates one (writing the overlay shape under the existing id or root). The Protect box was removed (it only fed the engine's sync strategy, which is not a UI surface) and with it the ctrl+d-swallowing TextArea; SharedRootScreen/QueueHelpScreen/_queue_help_text/parse_path_lines are deleted. Experimental claim: enforced for Claude tool calls, advisory beyond them.

Commands

  • Tests (Python): python3 -m pytest test/ -q. Single file: python3 -m pytest test/test_gc.py -q. Single test: python3 -m pytest test/test_gc.py::test_old_unnamed_session_is_deleted -q. Config in pytest.ini (asyncio_mode = auto); dev deps in test/requirements-dev.txt (pytest + pytest-asyncio). Textual is vendored, so nothing else needs installing.
  • Tests (shell): bats test/install.bats test/uninstall.bats test/hook.bats — shell-level coverage of install.sh, uninstall.sh, and the hook. (CLI subcommands are covered by pytest via subprocess in test_cli.py, so bats deliberately doesn't re-test them.)
  • CI: .github/workflows/ci.yml runs both suites on ubuntu + macos across Python 3.11–3.13 for every push/PR to main.
  • Install (dev, plain path): ./install.sh registers the hook in ~/.claude/settings.json and symlinks bin/session-explorer to ~/.local/bin/. It does NOT touch cleanupPeriodDays (retention is opt-in via the TUI prompt).
  • Install (marketplace path): /plugin marketplace add <this-repo> then /plugin install session-explorer. The plugin's bin/ is on the Bash-tool PATH automatically.
  • Uninstall: ./uninstall.sh (plain) or session-explorer uninstall (marketplace, since /plugin uninstall has no teardown hook). Both restore cleanupPeriodDays from the backup.
  • Releasing: every iteration of changes that ships needs a version bump and a GitHub release. Follow the cutting-a-release skill (.claude/skills/cutting-a-release/SKILL.md) — it is the authoritative checklist (bump __init__.py + plugin.json, update README/SPEC status lines and the help-screen keybindings if they changed, add a CHANGELOG.md section, then gh release create vX.Y.Z). Don't ship version-affecting work without running through it.

Implementation order

All milestones (M1–M7) are shipped as of v1.5.0; the SPEC.md milestone table records what each delivered. For new work, keep SPEC.md authoritative — update it in the same change rather than letting code and spec diverge.

Resolved design decisions

Previously open; settled during implementation. See SPEC.md → "Design decisions (resolved)" for the full log.

  • Claude's /rename JSONL format was reverse-engineered from a real transcript: a custom-title event. The index falls back to a display_name override only if that format ever proves volatile.
  • Preview pane content is settled: name, project, folder, branch, age, created date, message count, context size, session id, notes, first prompt, and transcript path (the never-populated summary block was dropped).

Two items remain deliberately deferred (see SPEC.md): in-place /compact and empty-folder pruning.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.