Skip to content

Instantly share code, notes, and snippets.

@digitalsignalperson
Created March 20, 2026 17:58
Show Gist options
  • Select an option

  • Save digitalsignalperson/9ab8541705c493bb0ba43494c366a6bb to your computer and use it in GitHub Desktop.

Select an option

Save digitalsignalperson/9ab8541705c493bb0ba43494c366a6bb to your computer and use it in GitHub Desktop.
Simple Claude Code Session Exporter
#!/usr/bin/env python3
"""
Simple Claude Code Session Exporter
Finds sessions, lists them, exports to markdown.
Inspired by https://github.com/jimmc414/cctrace/blob/master/export_claude_session.py
"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
def find_sessions(project_path):
"""Find all JSONL session files for the current project."""
project_path = str(project_path)
# Normalize path like Claude does
if os.name == 'nt': # Windows
project_dir_name = project_path.replace('\\', '-').replace(':', '-').replace('/', '-').replace('.', '-').replace('_', '-')
else: # Unix
normalized = project_path.replace('\\', '/')
project_dir_name = normalized.replace('/', '-').replace('.', '-').replace('_', '-')
if project_dir_name.startswith('-'):
project_dir_name = project_dir_name[1:]
claude_dir = Path.home() / '.claude' / 'projects'
if os.name == 'nt':
project_sessions_dir = claude_dir / project_dir_name
else:
project_sessions_dir = claude_dir / f'-{project_dir_name}'
if not project_sessions_dir.exists():
return []
# Get all JSONL files sorted by modification time (newest first)
files = []
for file in project_sessions_dir.glob('*.jsonl'):
if not file.name.startswith('agent-'): # Skip agent sessions
files.append({
'path': file,
'name': file.stem,
'mtime': file.stat().st_mtime
})
return sorted(files, key=lambda x: x['mtime'], reverse=True)
def parse_session(file_path):
"""Parse JSONL session file."""
messages = []
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
try:
messages.append(json.loads(line.strip()))
except json.JSONDecodeError:
continue
return messages
def format_message(msg_data):
"""Format a single message as markdown."""
if 'message' not in msg_data:
return ""
msg = msg_data['message']
timestamp = msg_data.get('timestamp', '')
lines = []
# Timestamp
if timestamp:
try:
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
lines.append(f"**[{dt.strftime('%Y-%m-%d %H:%M:%S')}]**")
except:
pass
# Role header
role = msg.get('role', 'unknown')
if role == 'user':
lines.append("\n### πŸ‘€ User\n")
elif role == 'assistant':
model = msg.get('model', 'Claude')
lines.append(f"\n### πŸ€– Assistant ({model})\n")
# Content
if 'content' in msg:
if isinstance(msg['content'], str):
lines.append(msg['content'])
elif isinstance(msg['content'], list):
for content in msg['content']:
if not isinstance(content, dict):
continue
ctype = content.get('type')
if ctype == 'text':
lines.append(content.get('text', ''))
elif ctype == 'thinking':
lines.append("\n<details>")
lines.append("<summary>πŸ’­ Reasoning</summary>\n")
lines.append("```")
lines.append(content.get('thinking', ''))
lines.append("```")
lines.append("</details>\n")
elif ctype == 'tool_use':
tool_name = content.get('name', 'unknown')
lines.append(f"\nπŸ”§ Tool: {tool_name}")
lines.append("```json")
lines.append(json.dumps(content.get('input', {}), indent=2))
lines.append("```\n")
elif ctype == 'tool_result':
lines.append("\nπŸ“Š Result:")
result = content.get('content', '')
if isinstance(result, str):
lines.append(result[:2000]) # Limit
else:
lines.append(str(result)[:2000])
lines.append("")
return '\n'.join(lines)
def main():
cwd = Path.cwd()
sessions = find_sessions(cwd)
if not sessions:
print("❌ No Claude Code sessions found in this project")
return 1
print(f"Found {len(sessions)} session(s):\n")
for i, session in enumerate(sessions):
age_seconds = int(datetime.now().timestamp() - session['mtime'])
age_str = format_age(age_seconds)
print(f"{i+1}. {session['name'][:20]:20} ({age_str})")
# Default to most recent
choice = input(f"\nSelect session [1]: ").strip() or "1"
try:
idx = int(choice) - 1
if idx < 0 or idx >= len(sessions):
print("Invalid selection")
return 1
except ValueError:
print("Invalid input")
return 1
selected = sessions[idx]
print(f"\nπŸ“€ Exporting {selected['name']}...")
messages = parse_session(selected['path'])
# Write markdown
output_file = f"claude_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(f"# Claude Code Session\n\n")
f.write(f"**Session:** `{selected['name']}`\n")
f.write(f"**Messages:** {len(messages)}\n")
f.write(f"**Exported:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("---\n\n")
for msg in messages:
formatted = format_message(msg)
if formatted:
f.write(formatted)
f.write("\n\n---\n\n")
print(f"βœ… Exported to: {output_file}")
return 0
def format_age(seconds):
"""Format age in seconds to readable string."""
if seconds < 60:
return f"{seconds}s ago"
elif seconds < 3600:
return f"{seconds//60}m ago"
elif seconds < 86400:
return f"{seconds//3600}h ago"
else:
return f"{seconds//86400}d ago"
if __name__ == '__main__':
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment