Skip to content

Instantly share code, notes, and snippets.

@Ar9av
Created June 19, 2026 13:44
Show Gist options
  • Select an option

  • Save Ar9av/d6249442a555e943c297a70b183ea538 to your computer and use it in GitHub Desktop.

Select an option

Save Ar9av/d6249442a555e943c297a70b183ea538 to your computer and use it in GitHub Desktop.
claude-session-load — Claude Code skill: load a Claude Code session by id into context
#!/usr/bin/env python3
"""Locate a Claude Code session file by (possibly partial/typo'd) session id
and extract a structured digest: session metadata, real user messages,
assistant text, and tool calls. Stdlib only.
Usage:
load_session.py <session-id-or-substring>
Prints JSON to stdout:
{"status": "ok", "path": "...", "digest": {...}}
{"status": "ambiguous", "candidates": [...]}
{"status": "not_found", "searched": [...]}
"""
import difflib
import glob
import json
import os
import sys
ROOTS = [
os.path.expanduser("~/.claude/projects"),
os.path.expanduser(
"~/Library/Application Support/Claude/local-agent-mode-sessions"
),
]
BOILERPLATE_PREFIXES = (
"<local-command-caveat>",
"<command-name>",
"<command-message>",
"<command-args>",
"<system-reminder>",
)
def find_candidates(query):
files = []
for root in ROOTS:
if os.path.isdir(root):
files.extend(glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True))
query = query.strip()
exact = [f for f in files if os.path.basename(f) == f"{query}.jsonl"]
if exact:
return exact
substring = [f for f in files if query in os.path.basename(f)]
if substring:
return substring
scored = []
for f in files:
candidate_id = os.path.basename(f)[: -len(".jsonl")]
ratio = difflib.SequenceMatcher(None, query, candidate_id).ratio()
if ratio >= 0.6:
scored.append((ratio, f))
scored.sort(key=lambda x: x[0], reverse=True)
if not scored:
return []
best = scored[0][0]
return [f for ratio, f in scored if ratio >= best - 0.05]
def is_boilerplate_text(text):
stripped = text.strip()
if not stripped:
return True
return any(stripped.startswith(p) for p in BOILERPLATE_PREFIXES)
def truncate(s, n=600):
s = s if isinstance(s, str) else json.dumps(s)
return s if len(s) <= n else s[:n] + f"... [+{len(s) - n} chars]"
def extract_text_blocks(content):
"""content can be a plain string or a list of content blocks."""
texts = []
if isinstance(content, str):
if not is_boilerplate_text(content):
texts.append(content)
return texts
if isinstance(content, list):
for block in content:
if not isinstance(block, dict):
continue
if block.get("type") == "text" and not is_boilerplate_text(block.get("text", "")):
texts.append(block["text"])
return texts
def extract_tool_uses(content):
uses = []
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "tool_use":
uses.append({"name": block.get("name"), "input": block.get("input")})
return uses
def parse_session(path):
meta = {}
user_msgs = []
assistant_msgs = []
tool_uses = []
with open(path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
d = json.loads(line)
except json.JSONDecodeError:
continue
dtype = d.get("type")
ts = d.get("timestamp")
if not meta and dtype in ("user", "assistant") and d.get("sessionId"):
meta = {
"id": d.get("sessionId"),
"cwd": d.get("cwd"),
"gitBranch": d.get("gitBranch"),
"version": d.get("version"),
"timestamp": ts,
}
if dtype == "user" and not d.get("isMeta"):
message = d.get("message", {})
for text in extract_text_blocks(message.get("content")):
user_msgs.append({"ts": ts, "text": truncate(text, 1500)})
elif dtype == "assistant":
message = d.get("message", {})
content = message.get("content")
for text in extract_text_blocks(content):
assistant_msgs.append({"ts": ts, "text": truncate(text, 1500)})
for use in extract_tool_uses(content):
tool_uses.append({
"ts": ts,
"name": use["name"],
"input": truncate(use["input"]),
})
return {
"meta": meta,
"user_message_count": len(user_msgs),
"assistant_message_count": len(assistant_msgs),
"tool_use_count": len(tool_uses),
"user_messages": user_msgs,
"assistant_messages": assistant_msgs,
"tool_uses": tool_uses,
}
def main():
if len(sys.argv) < 2:
print(json.dumps({"status": "error", "message": "usage: load_session.py <session-id>"}))
sys.exit(1)
query = sys.argv[1]
candidates = find_candidates(query)
if not candidates:
print(json.dumps({"status": "not_found", "searched": ROOTS}))
return
if len(candidates) > 1:
print(json.dumps({"status": "ambiguous", "candidates": sorted(candidates)}))
return
path = candidates[0]
digest = parse_session(path)
print(json.dumps({"status": "ok", "path": path, "digest": digest}, indent=2))
if __name__ == "__main__":
main()
name claude-session-load
description Load a specific Claude Code session into the current conversation by session id, given as a rough/possibly-mistyped uuid (e.g. "01935a40-5a79-4142-966a-47a1da99c3bb"). Reads the matching <session-id>.jsonl under ~/.claude/projects/**/ (and Claude desktop's local-agent-mode-sessions), produces a concise summary of what was asked, done, and changed, and keeps the raw file path on hand to look up exact detail later in the conversation. Use when the user invokes /claude-session-load <id>, says "load claude session <id>", "what happened in claude session <id>", "summarize this claude session", or pastes a Claude session id and asks what it contains.

Claude Session Load

Bring one Claude Code session into context as a compact summary, not a wall of raw JSONL. The point is to load enough to answer questions about that session immediately, while keeping a path back to the raw transcript for anything the summary doesn't cover.

This is the Claude-side counterpart to codex-session-load — same idea, different file format.

Step 1: Resolve the session id

Take whatever id-like string the user gives you (full uuid, partial, or slightly mistyped) and run the bundled script:

python3 ~/.claude/skills/claude-session-load/scripts/load_session.py "<id-or-substring>"

This searches ~/.claude/projects/**/*.jsonl and Claude desktop's ~/Library/Application Support/Claude/local-agent-mode-sessions/**/*.jsonl, first by exact filename match (<id>.jsonl), then substring, then a fuzzy match against the filename's id portion (handles a stray/missing character from copy/paste). It returns one of:

  • {"status": "ok", "path": ..., "digest": {...}} — exactly one match, parsed.
  • {"status": "ambiguous", "candidates": [...]} — more than one close match. List the candidate paths (the parent directory name encodes the project path, e.g. -Users-name-project-a/Users/name/project-a) and ask the user which one they meant.
  • {"status": "not_found", ...} — no match. Tell the user and ask them to double check the id, or offer to list recent sessions for a project (ls ~/.claude/projects/<project-dir>/*.jsonl).

Step 2: Build the summary from the digest

The digest already strips boilerplate (<local-command-caveat>, <command-name>, <system-reminder> wrapper text, isMeta lines) and gives you:

  • meta — session id, cwd, git branch, Claude Code version, start time
  • user_messages — the actual things the user asked for, in order
  • assistant_messages — the model's text replies
  • tool_uses — tool calls made (name + truncated input), e.g. Edit/Bash/Write calls

Turn this into a short narrative for the user, in roughly this shape:

  1. One-line header: project (cwd), git branch, date, how many turns.
  2. Timeline: each real user ask, in order, with a short note on what was done in response (cross-reference tool_uses near that timestamp — e.g. "edited X", "ran git push", "created Y").
  3. Outcome: what shipped / what's still open, if evident from the trailing messages.

Keep it tight — a few bullets per turn, not a transcript dump. This summary is what should live in the conversation; don't paste the full raw JSON back to the user unless they ask for it.

Step 3: Keep the raw file on hand for follow-ups

Remember the resolved path from Step 1. If the user later asks something the summary doesn't cover (the exact diff, an exact tool input, an exact error string), don't re-derive it from memory — grep the original file for it:

grep -o '"name":"[A-Za-z]*"' <path> | sort -u           # which tools were used
grep -n '<keyword>' <path>                               # find line(s) mentioning something

Then read the matched line(s) (python3 -c "import json; print(json.loads(open('<path>').readlines()[N]))") and answer from that specific event rather than guessing. Treat the summary as the working context and the file as the source of truth to refer back to on demand.

Notes

  • Claude Code session ids are the filename itself: ~/.claude/projects/<path-encoded-project>/<uuid>.jsonl.
  • message.content can be a plain string (simple text turns) or a list of content blocks (text, tool_use, tool_result, etc.) — the script already handles both.
  • If several sessions are genuinely close matches (forked/resumed sessions, or the same id reused across a CLI session and a desktop local-agent-mode session), surface all of them rather than guessing — let the user disambiguate.
  • This skill only loads context; it does not write anything to the wiki. For mining Claude history into the Obsidian wiki, use claude-history-ingest or wiki-agent (/wiki-claude) instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment