
Session Recover
- 171 installs
- 2 repo stars
- Updated July 29, 2026
- ngmeyer/skills
Helps with ai & agent building tasks.
About
session-recover is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- session-recover
- AI & Agent Building
- AI-coding skill
Session Recover by the numbers
- 171 all-time installs (skills.sh)
- +13 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #3,124 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ngmeyer/skills --skill session-recoverAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 171 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 29, 2026 |
| Repository | ngmeyer/skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
/session-recover — Manually merge duplicate Claude Code project directories
Claude Code stores per-project session transcripts and persistent memory under ~/.claude/projects/{encoded-cwd}/. The encoded cwd is the absolute path with every non-alphanumeric character replaced by -. Two different cwd paths that point at the same logical project (a real directory + its symlink mirror, an external-mount path + a ~/Projects/ shortcut, a Linux user moving from /home/me to /Users/me, etc.) produce two completely separate namespaces — separate session lists in /resume, separate memory dirs, no cross-visibility.
This skill cleans that up: detect the duplicates, unify the memory into the canonical location, archive the orphan transcripts, and capture the lesson so future sessions don't re-create the split.
When this skill applies
- You opened a session and the memory dir is empty, but you remember adding entries in past sessions
claude --resumeshows fewer sessions than you expect- You find two
~/.claude/projects/-*-foo/directories whose trailing tokens match - Cross-session memory references in conversation don't resolve (
[[some-memory]]links to nothing) - A grep for the project name across
~/.claude/projects/returns multiple matches
If none of the above hold, the skill has nothing to do — exit early and tell the user.
Core principle
The canonical path is the one Claude Code's current system prompt tells you it is. When the harness starts a session it announces:
You have a persistent, file-based memory system at /Users/me/.claude/projects/{ENCODED}/memory/That's the winner. Everything else with a similar trailing token is a loser candidate. The user can override if they actually want a different path canonical, but default to what the harness chose.
Procedure
Phase 1 — IDENTIFY: find duplicate project dirs
Use the optional argument as a project-name token. If omitted, infer from cwd's trailing component (basename "$PWD") or the conversation.
bash ~/.claude/skills/session-recover/scripts/inventory.sh <project-token>The script lists every ~/.claude/projects/*<token>*/ directory with:
- jsonl session file count + total size + newest-file mtime
- memory dir entry count
- whether the encoded path appears to be Lexar (
Volumes-Lexar), home-Projects (Users-.*-Projects), or something else
Stop and ask the user if:
- Only one match exists → no duplicates → exit
- Three or more matches exist → confirm which is canonical before proceeding
- The candidates have wildly different jsonl mtimes (e.g. one was active last week, another was active today) → confirm the user wants both folded together
Phase 2 — IDENTIFY: pick the winner
Default: the path mentioned in the current session's system prompt under "persistent, file-based memory system at …".
If that path isn't a duplicate-dir candidate (e.g. user is invoking this skill from outside the project), ask the user which one they want canonical. Usually it's the path they actually cd into when starting work, or the path called out as "canonical" in any project-level CLAUDE.md.
Phase 3 — MERGE memory
For each loser, move its memory contents into the winner. Always `mv`, never `cp`, so the loser's memory dir empties out and stops being a competing source of truth.
WINNER=~/.claude/projects/<canonical-encoded-path>
LOSER=~/.claude/projects/<loser-encoded-path>
mkdir -p "$WINNER/memory"
mv "$LOSER/memory/"* "$WINNER/memory/" 2>/dev/null
rmdir "$LOSER/memory" 2>/dev/nullConflict handling: if both sides have a file with the same name (most commonly MEMORY.md), do not clobber. Read both, write a unified version into the winner by hand, delete the loser's copy after. This usually only affects MEMORY.md (the index file).
Phase 4 — ARCHIVE jsonl transcripts
Move the loser's jsonl session files and any per-session subdirs to ~/.claude/archive/. Don't `rm` — archive is reversible, deletion isn't.
ARCHIVE_DIR=~/.claude/archive/$(basename "$LOSER")
mkdir -p "$ARCHIVE_DIR"
mv "$LOSER"/*.jsonl "$ARCHIVE_DIR/" 2>/dev/null
# Also archive any UUID subdirs (per-session checkpoint dirs)
for d in "$LOSER"/[0-9a-f]*-[0-9a-f]*/; do
[ -d "$d" ] && mv "$d" "$ARCHIVE_DIR/"
done
rmdir "$LOSER" 2>/dev/nullIf rmdir "$LOSER" fails because the directory isn't empty, list what's still there and ask the user — there's usually a stray file (.DS_Store, a non-standard subdir) that needs explicit handling.
Phase 5 — CAPTURE the lesson as memory
Write a feedback-type memory entry into the winner's memory dir so a future session knows the dual-cwd hazard exists for this project:
---
name: dual-cwd-memory-split-{project}
description: Memory dir is path-derived from cwd. Two cwd paths for the same project produced separate memory namespaces; merged YYYY-MM-DD.
metadata:
type: feedback
---
The {project} project is reachable via:
- `{canonical-cwd-path}` — canonical
- `{loser-cwd-path}` — symlink/mirror/alias
These produce separate `~/.claude/projects/-*/` namespaces. Memory was merged into the canonical path on YYYY-MM-DD; jsonl transcripts archived to `~/.claude/archive/{loser-encoded}/`.
**How to apply:** always `cd` into `{canonical-cwd-path}` before `claude`. If a future session lands at the other path and finds an empty memory dir, re-run /session-recover before doing real work.Then add a one-line pointer to MEMORY.md in the winner's memory dir:
- [Dual-cwd memory split](feedback_dual_cwd_memory_split_{project}.md) — memory namespaces merged YYYY-MM-DD; cd into the canonical pathPhase 6 — VERIFY
ls "$WINNER/memory/" | wc -l # entry count, should be sum of both sides minus dedup
ls "$LOSER" 2>/dev/null # should report "No such file or directory"
ls ~/.claude/archive/$(basename "$LOSER")/ # archived jsonls + dirs(V2) Reconciliation gate — prove nothing was lost. Before declaring success, reconcile the counts: capture winner_before and loser_count in Phase 3 before moving, then assert winner_after == winner_before + loser_count − dedup_count, where dedup_count is the number of same-name files you hand-merged. If the arithmetic doesn't close, STOP and show the discrepancy — a missing file means a silent loss, which is the one outcome this skill must never produce. Only report success once the count reconciles (or the user accepts a documented dedup).
Report the final state to the user as a 3-row table, including the reconciliation line (winner_before + loser − dedup = winner_after).
Phase 7 — SUGGEST /compact
Claude Code's /compact slash command folds the running session into a clean summary. After a merge the current conversation contains a lot of "found this, moved that" detail that won't be useful later. Tell the user to run /compact (you can't invoke it yourself — it's a user-level command).
Variant: legacy orphan memory (assess, don't blind-merge)
Phases 3–4 above assume the loser is a live mirror of the same current work — so moving its memory into the winner is safe. But sometimes the orphan is a legacy namespace from before a project folder moved (e.g. the project was at /Users/me/Projects/foo for months, then moved to /Volumes/Drive/Projects/foo). Then the orphan's memory is old — pre-move status, done backlogs, superseded facts. Blind-merging it into the canonical dir re-pollutes current memory with stale content. Detect this when the orphan's memory mtimes are weeks older than the canonical dir's, or the orphan's MEMORY.md describes a clearly earlier project state.
In that case, replace Phase 3's mv-everything with a per-file assessment:
1. Read every orphaned memory file. Classify each (verify claims against the current repo/code — files move, features ship, facts drift):
- KEEPER → repo: a decision, gotcha, or product idea a remote agent/collaborator needs that ISN'T already in the repo (
docs/,CLAUDE.md) or canonical memory. → write it into the repo (ADR underdocs/decisions/, adocs/knowledge doc, or an ideas backlog). - KEEPER → local: still-valid behavior guidance ("don't do X, it's already handled"). → copy into the canonical memory dir + index it.
- SECRET-LOCATION: anything naming where a token/credential lives, or env→environment maps. → fold into the canonical secrets-inventory memory. Never commit, even to a private repo.
- STALE: superseded status, finished backlogs, old audits. → archive.
2. Recover the keepers to their destinations (repo or canonical memory); reconcile secret-locations into the local inventory. 3. Archive the stale orphan files to an _archived-pre-migration/ subfolder inside the orphan memory dir — don't delete, don't merge into canonical. 4. Tombstone the orphan's MEMORY.md: replace it with a note that this is the orphaned <old-path> mirror, the canonical namespace + repo are the source of truth, and the files were archived. This stops a future session opened from the old path from trusting stale content. 5. Report a table: orphaned file → verdict → action. Call out any genuinely valuable recovered item prominently — that's the payoff (e.g. a parked product idea that never made it into a backlog).
Skip Phases 4 (transcript archive — leave legacy transcripts alone unless asked) and 5 (the dual-cwd feedback memory is still worth writing in the canonical dir so the split doesn't recur).
What this skill does NOT do
- Does not merge two jsonl transcripts into a single session. That's not possible — Claude Code has no merge operation. The skill keeps the active session's transcript and archives the others.
- Does not edit code, CLAUDE.md, or plan docs unless the user explicitly asks for that as a follow-up. Memory-and-jsonls only. If the project has a stale plan/status doc that should reflect the merge, the user can ask you to update it after the skill runs.
- Does not delete anything. Archive is reversible;
rmis not. - Does not run on every invocation. If Phase 1 finds zero duplicates, exit early and say so. Don't manufacture work.
Gotchas
1. `mv` vs `cp`: always mv. If you cp and forget to delete the source, the loser's memory dir keeps drifting as future sessions land there and write new entries. Deletion at the source is what stops the bleed.
2. MEMORY.md merge collision: the index file usually exists in both. Don't mv blindly — mv -n will keep the loser's copy as a sibling and you'll end up with two indexes. Read both, hand-merge, delete loser's copy.
3. Per-session UUID subdirs: Claude Code creates <uuid>/ checkpoint dirs alongside <uuid>.jsonl files. These are mostly resumable-session state. Archive them along with the jsonl; don't leave orphans.
4. `rmdir "$LOSER"` failing: usually a .DS_Store on macOS or a stray todos/ dir. List the contents before retrying; don't rm -rf reflexively.
5. The current session might be in the loser dir. If the user opened a session from the non-canonical cwd, the current .jsonl is being written to the loser. Don't archive it mid-conversation — the running session's writes will fail. Either tell the user to /exit first, or skip the current session's jsonl and archive the rest.
6. Cross-project name collisions: if two project names share a token (e.g. api and api-gateway), the inventory script will return both. Always show the user the full candidate list and confirm before moving anything.
7. Don't try to be clever about "which transcript is more recent." That's a merge-content question, not a merge-state question. The skill's job is to unify the memory namespace; the active session's transcript stays where it is.
Changelog
V2 (2026-05-27)
Optimized via skillforge optimize. Honest note: external outcome research was thin — this is a procedural skill for one specific Claude Code mechanism, with no meaningful state-of-the-art to mine. The genuine outcome to protect is zero memory loss on merge, so the V2 change is a safety hardening, not a research import:
- Reconciliation gate (Phase 6) — assert
winner_after == winner_before + loser − dedup; STOP on any mismatch. Turns "looks done" into "provably lost nothing." Pairs with the existing archive-never-delete rule (the merge is already reversible). - No outcome-research-driven additions were forced (the agent-memory three-layer model is already reflected in the legacy-orphan variant's repo/local/secret routing).
See also
references/merge-example.md— sanitized end-to-end walkthrough of a dual-cwd merge.scripts/inventory.sh— the Phase 1 helper.- The legacy-orphan variant recovers keepers into the repo (ADRs, docs, ideas backlog) for the repo-side migration; this skill handles the namespace cleanup + tombstone.
Reference example — duplicate project-dir merge
Sanitized walkthrough of a dual-cwd merge. Generic project (acme-api); expect your specifics to differ.
Symptom
Mid-conversation in a session opened from /mnt/work/acme-api/, tried to read project memory entries that "should be there" — found the dir empty:
$ ls ~/.claude/projects/-mnt-work-acme-api/memory/
(empty)But a sibling dir under a different encoded path had everything:
$ ls ~/.claude/projects/-Users-me-Projects-acme-api/memory/ | wc -l
22Same project, two cwd paths (/mnt/work/acme-api vs ~/Projects/acme-api — the latter is a home-dir symlink mirror of the canonical mount), two namespaces.
Phase 1 — Inventory
$ bash ~/.claude/skills/session-recover/scripts/inventory.sh acme-api
Found 3 candidate dirs matching 'acme-api':
PATH JSONL TOTAL_BYTES NEWEST_MTIME MEM CLASS
---- ----- ----------- ------------ --- -----
-Users-me-Projects-acme-api-web 1 618787 2026-04-19_18:48 - ~/Projects mirror
-mnt-work-acme-api 2 22633824 2026-05-15_17:15 26 external/secondary drive
-mnt-work-acme-api-worker 1 3214 2026-05-10_08:00 - external/secondary driveThree candidates surfaced. The bottom two (-web and -worker) are subdir-scoped sessions from when the user cd'd into web/ or worker/ directly — those are SEPARATE projects from the acme-api workspace and should be left alone, not merged.
The one to merge: -Users-me-Projects-acme-api — same logical project as -mnt-work-acme-api via the symlink mirror.
Phase 2 — Pick winner
Current session's system prompt said:
You have a persistent, file-based memory system at ~/.claude/projects/-mnt-work-acme-api/memory/So the mount-encoded dir is canonical. Loser = -Users-me-Projects-acme-api.
Phase 3 — Merge memory
WINNER=~/.claude/projects/-mnt-work-acme-api
LOSER=~/.claude/projects/-Users-me-Projects-acme-api
mv "$LOSER/memory/"* "$WINNER/memory/"
rmdir "$LOSER/memory"22 memory files moved. No conflicts (winner's memory dir was empty — easy case).
Phase 4 — Archive jsonls
ARCHIVE=~/.claude/archive/-Users-me-Projects-acme-api
mkdir -p "$ARCHIVE"
mv "$LOSER"/*.jsonl "$LOSER"/5f1edf0a-789b-404b-8fc2-ebf1791ea9f2 "$LOSER"/d893cdad-e5dd-480c-9ea2-002bf360d660 "$ARCHIVE/"
rmdir "$LOSER"Two *.jsonl files plus their per-session UUID subdirs moved. Old project dir empty → removed.
Phase 5 — Capture lesson
Wrote a feedback-type memory into the winner's memory dir explaining:
- The two cwd paths
- Which is canonical
- When the merge happened
- "always
cdinto the canonical mount path beforeclaude"
Added a one-line index pointer to MEMORY.md in the winner's memory dir.
Phase 6 — Verify
$ ls ~/.claude/projects/-mnt-work-acme-api/memory/ | wc -l
26 # was 22 from loser + 4 fresh entries this session
$ ls ~/.claude/projects/-Users-me-Projects-acme-api 2>&1
ls: ...: No such file or directory # gone
$ ls ~/.claude/archive/-Users-me-Projects-acme-api/
5f1edf0a-...jsonl 5f1edf0a-.../ d893cdad-...jsonl d893cdad-.../Phase 7 — /compact
Run /compact to fold the merge details into the session summary.
What surprised me
- One side had 22 entries, the other had zero — that's the easy case. If both sides had been populated I would have had to hand-merge
MEMORY.md(the index file) sincemvwould have refused to clobber.
- The inventory script found `-web` and `-worker` candidates that look like duplicates but aren't. They're from sessions opened in subdirectories of the workspace, a legitimate use of the workspace structure. Don't merge subdir-scoped sessions into the workspace dir; they're separate projects to Claude Code.
- The "wrong-path" memory dir was months old. Sessions had been silently writing memory to it for a long time. No one noticed because every session that opened from the wrong cwd found a populated memory dir and assumed it was authoritative. The split only became visible when a session opened from the canonical cwd and found an empty dir. That's the failure mode the captured-lesson memory entry exists to prevent.
#!/usr/bin/env bash
# Inventory candidate duplicate Claude Code project dirs for /session-recover.
#
# Usage:
# inventory.sh # uses basename "$PWD" as project token
# inventory.sh <token> # explicit token, e.g. "myapp"
#
# Lists every ~/.claude/projects/*<token>*/ dir with:
# - jsonl file count, total bytes, newest mtime
# - memory dir entry count
# - rough classification of the encoded path
#
# Exits 0 if at least one candidate found, 1 if none.
set -euo pipefail
TOKEN="${1:-$(basename "${PWD:-/}")}"
if [ -z "$TOKEN" ] || [ "$TOKEN" = "/" ]; then
echo "ERROR: pass a project-name token, e.g. 'inventory.sh myapp'" >&2
exit 2
fi
PROJECTS_ROOT="$HOME/.claude/projects"
if [ ! -d "$PROJECTS_ROOT" ]; then
echo "ERROR: $PROJECTS_ROOT does not exist" >&2
exit 2
fi
CANDIDATES=()
while IFS= read -r line; do
CANDIDATES+=("$line")
done < <(find "$PROJECTS_ROOT" -maxdepth 1 -type d -iname "*${TOKEN}*" 2>/dev/null | sort)
if [ "${#CANDIDATES[@]}" -eq 0 ]; then
echo "No candidate dirs match token '$TOKEN' under $PROJECTS_ROOT"
exit 1
fi
if [ "${#CANDIDATES[@]}" -eq 1 ]; then
echo "Only one candidate matches '$TOKEN' — no duplicates to merge:"
echo " ${CANDIDATES[0]}"
exit 0
fi
echo "Found ${#CANDIDATES[@]} candidate dirs matching '$TOKEN':"
echo
# Header
printf "%-60s %5s %12s %19s %5s %s\n" \
"PATH" "JSONL" "TOTAL_BYTES" "NEWEST_MTIME" "MEM" "CLASS"
printf "%-60s %5s %12s %19s %5s %s\n" \
"----" "-----" "-----------" "------------" "---" "-----"
for d in "${CANDIDATES[@]}"; do
short="${d#$PROJECTS_ROOT/}"
jsonl_count=$(find "$d" -maxdepth 1 -name "*.jsonl" 2>/dev/null | wc -l | tr -d ' ')
if [ "$jsonl_count" -gt 0 ]; then
total_bytes=$(find "$d" -maxdepth 1 -name "*.jsonl" -exec stat -f '%z' {} \; 2>/dev/null \
| awk '{s+=$1} END{print s+0}')
newest=$(find "$d" -maxdepth 1 -name "*.jsonl" -exec stat -f '%Sm' -t '%Y-%m-%d_%H:%M' {} \; 2>/dev/null \
| sort | tail -1)
else
total_bytes=0
newest="-"
fi
if [ -d "$d/memory" ]; then
mem_count=$(find "$d/memory" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')
else
mem_count="-"
fi
# Rough classification
case "$short" in
*Volumes-*) klass="external/secondary drive" ;;
*mnt-*) klass="external/secondary drive" ;;
*Users-*-Projects-*) klass="~/Projects mirror" ;;
*home-*) klass="Linux home" ;;
*) klass="other" ;;
esac
printf "%-60s %5s %12s %19s %5s %s\n" \
"$short" "$jsonl_count" "$total_bytes" "$newest" "$mem_count" "$klass"
done
echo
echo "Next: pick the canonical winner (default = the path mentioned in the"
echo "current session's system prompt under 'persistent, file-based memory"
echo "system at …'). The other(s) are losers; merge their memory + archive"
echo "their jsonls per the SKILL.md procedure."