Claude Codeセッションログを一括MD化!拡張版エクスポーターで開発記録を資産化

Development
スポンサーリンク

Claude Codeで開発を進めると、セッション毎に貴重なやり取りが蓄積されていきます。しかし、そのログファイル(JSONL形式)は人間が読める形式ではありません。この記事では、以前紹介したClaude Chat Exporterを大幅に拡張し、Claude Codeのセッションログを一括でMarkdown化できる神機能を実装したツールを紹介します。

スポンサーリンク

なぜClaude Codeのセッションログを管理するのか?

Claude Codeでの開発セッションには、通常のチャット以上の価値があります:

  • 実装の全プロセス: 設計から実装、テスト、デバッグまでの完全な記録
  • コンテキスト付きの会話: ファイル編集、コマンド実行の履歴と結果
  • 問題解決の軌跡: エラーとその解決方法の詳細な記録
  • アーキテクチャ判断: 技術選定や設計判断の理由
  • ベストプラクティス: Claude Codeとの協働で得られた知見

これらを適切に管理することで:

  • ✅ プロジェクトの開発履歴として参照できる
  • ✅ 同じ問題に遭遇した時の解決策を即座に見つけられる
  • ✅ チーム内でClaude Code活用のノウハウを共有できる
  • ✅ 技術ブログやドキュメントの素材になる
  • ✅ 開発プロセスの振り返りと改善に活用できる

Amazon検索[本 Python]

以前のツールとの違い

以前の記事で紹介したClaude Chat Exporterは、claude.aiのWebチャット用でした。今回の拡張版では、以下の新機能を追加しています:

新機能一覧

機能旧バージョン新バージョン
対応形式JSON(conversations.json)のみJSON + JSONL(Claude Code)
ファイル処理単一ファイル複数ファイル一括処理
入力方法ダイアログのみダイアログ + ドラッグ&ドロップ + コマンドライン
セッション識別チャットタイトルのみセッションID + フォルダ名
メッセージフィルタなしシステムメッセージ、ツール結果を自動除外
コンテンツ処理シンプルテキスト配列形式、複雑な構造に対応

Claude CodeのJSONL形式とは?

Claude Codeは各セッションのログを .jsonl(JSON Lines)形式で保存します:

C:\Users\<ユーザー名>\.claude\sessions\<セッションID>\transcript.jsonl

各行が独立したJSONオブジェクトで、以下のような情報が含まれます:

{"type":"user","sessionId":"abc123...","message":{"role":"user","content":"関数を作成して"}}
{"type":"assistant","sessionId":"abc123...","message":{"role":"assistant","content":"承知しました..."}}

この形式は機械的な処理には適していますが、人間が読むには不向きです。

ツールの全機能

1. 複数ファイル一括処理

exporter.py にファイルを複数ドロップ → 一括処理

# コマンドライン
python exporter.py file1.jsonl file2.json file3.jsonl

# ダイアログ
python exporter.py → 複数ファイル選択可能

2. JSON/JSONL両対応

  • JSON: claude.aiのWebチャットエクスポート(conversations.json)
  • JSONL: Claude Codeのセッションログ(transcript.jsonl)

3. 賢いフィルタリング

自動で不要なメッセージを除外:

  • システムメッセージ(isMeta: true
  • ツール実行結果(tool_resultタイプ)
  • コンテキスト情報のみのメッセージ

4. セッション識別

Claude Codeのセッションログでは:

  • セッションIDの先頭8文字を抽出
  • フォルダ名も含めて識別
  • ファイル名: <フォルダ名>_chat_<セッションID>.md

例:my-project_chat_abc12345.md

5. 安全なファイル名生成

Windows/Mac/Linuxで使えない文字を自動変換:

  • \ / : * ? " < > |_

使い方

ステップ1: Claude Codeのセッションログを見つける

# Windowsの場合
C:\Users\<ユーザー名>\.claude\sessions\

# macOS/Linuxの場合
~/.claude/sessions/

各セッションフォルダ内に transcript.jsonl があります。

ステップ2: エクスポーターを配置

# フォルダ作成
mkdir C:\my-local\my-python\claude_chat_exporter
cd C:\my-local\my-python\claude_chat_exporter

# スクリプト配置(後述のコードを保存)

Windows ユーザー向け: ドラッグ&ドロップで実行するために、exporter.bat も作成してください:

@echo off
python "%~dp0exporter.py" %*
pause

このバッチファイルを exporter.py と同じフォルダに保存することで、ファイルをドラッグ&ドロップして実行できるようになります。

ステップ3: 実行

方法1: ドラッグ&ドロップ(一番簡単!)

Windows の場合: .py ファイルへのドラッグ&ドロップは関連付けの問題で動作しないことがあります。その場合は exporter.bat(後述)を使用してください。

  1. 必要な transcript.jsonl ファイルを複数選択
  2. exporter.bat(Windows)または exporter.py(Mac/Linux)にドラッグ&ドロップ
  3. 自動処理完了!

方法2: ダイアログ

python exporter.py

ファイル選択ダイアログで複数のJSON/JSONLファイルを選択

方法3: コマンドライン

# 単一ファイル
python exporter.py C:\Users\user\.claude\sessions\abc123\transcript.jsonl

# 複数ファイル
python exporter.py session1\transcript.jsonl session2\transcript.jsonl

# ワイルドカード(PowerShell)
python exporter.py (Get-ChildItem -Path "C:\Users\user\.claude\sessions\*\transcript.jsonl" -Recurse).FullName

ステップ4: 出力確認

# 出力先
output\20260215_100000\
├── session1_chat_abc12345.md
├── session2_chat_def67890.md
└── project-x_chat_ghi11111.md

ソースコード全文

exporter.py

import json
import sys
import tkinter.filedialog as fd
from datetime import datetime
from pathlib import Path
import re


def clean_system_tags(text):
    """Claude Codeのシステムタグを除去"""
    # システムタグのパターン(開始タグと終了タグのペア)
    paired_tags = [
        r'.*?',
        r'.*?',
        r'.*?',
        r'.*?',
        r'.*?',
        r'.*?',
        r'.*?',
        r'.*?',
    ]

    # 各パターンを除去(DOTALLフラグで改行を含む)
    for pattern in paired_tags:
        text = re.sub(pattern, '', text, flags=re.DOTALL)

    # 連続する空行を1つにまとめる
    text = re.sub(r'\n{3,}', '\n\n', text)

    return text.strip()


def process_jsonl_file(file_path, output_dir):

    """JSONL形式(Claude Codeのログ)の処理"""
    messages = []
    session_id = None

    # 各行を読み込んでパース
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue

            try:
                entry = json.loads(line)

                # セッションID取得(タイトル用)
                if session_id is None and 'sessionId' in entry:
                    session_id = entry['sessionId']

                # ユーザーまたはアシスタントのメッセージのみ抽出
                if entry.get('type') in ['user', 'assistant'] and 'message' in entry:
                    msg = entry['message']

                    # isMeta=trueのメッセージはスキップ(システムメッセージ)
                    if entry.get('isMeta', False):
                        continue

                    role = msg.get('role', '')
                    content = msg.get('content', '')

                    # contentが配列形式の場合
                    if isinstance(content, list):
                        # tool_resultはスキップ(Task実行結果など)
                        if any(item.get('type') == 'tool_result' for item in content):
                            continue

                        # textタイプのコンテンツのみ抽出(user/assistant共通)
                        text_parts = [item.get('text', '') for item in content if item.get('type') == 'text']
                        content = '\n\n'.join(text_parts)

                    # システムタグを除去
                    content = clean_system_tags(content) if isinstance(content, str) else content

                    if content and role:
                        # roleを表示形式に変換
                        sender = 'human' if role == 'user' else 'assistant'
                        messages.append({'sender': sender, 'text': content})
            except json.JSONDecodeError:
                continue

    # メッセージが1つもない場合はスキップ
    if not messages:
        print(f"スキップ: メッセージが含まれていません ({file_path.name})")
        return

    # フォルダ名とタイトル生成
    folder_name = file_path.parent.name
    if session_id:
        title = f"chat_{session_id[:8]}"
    else:
        title = file_path.stem

    # ファイル名に使えない文字を除去(フォルダ名_タイトル.md)
    safe_folder_name = re.sub(r'[\\/:*?"<>|]', '_', folder_name)
    safe_title = re.sub(r'[\\/:*?"<>|]', '_', title)
    filename = f"{safe_folder_name}_{safe_title}.md"

    # Markdownファイルに出力
    with open(output_dir / filename, 'w', encoding='utf-8') as f:
        f.write(f"# {safe_folder_name}_{safe_title}\n\n")

        for m in messages:
            f.write(f"**[{m['sender']}]**:\n\n{m['text']}\n\n")

    print(f"処理完了: {filename}")


def process_json_file(file_path, output_dir):
    """JSON形式(conversations.json)の処理"""
    try:
        d = json.load(open(file_path, 'r', encoding='utf-8'))
    except:
        d = json.load(open(file_path, 'r', encoding='cp932'))

    # チャット毎にファイル出力
    for i, c in enumerate(d):
        # タイトル取得(なければ chat_N)
        title = c.get('name', f'chat_{i}')

        # フォルダ名を取得
        folder_name = file_path.parent.name
        safe_folder_name = re.sub(r'[\\/:*?"<>|]', '_', folder_name)
        safe_title = re.sub(r'[\\/:*?"<>|]', '_', title)
        filename = f"{safe_folder_name}_{safe_title}.md"

        # Markdownファイルに出力
        with open(output_dir / filename, 'w', encoding='utf-8') as f:
            # タイトルを見出しとして出力
            f.write(f"# {safe_folder_name}_{safe_title}\n\n")

            # メッセージを順番に出力
            for m in c.get('chat_messages', []):
                f.write(f"**[{m['sender']}]**:\n\n{m['text']}\n\n")  # human または assistant

        print(f"処理完了: {filename}")


def main():
    # 出力フォルダ作成(pyファイルと同じ場所/output/yyyymmdd_hh24miss)
    output_dir = Path(__file__).parent / 'output' / datetime.now().strftime('%Y%m%d_%H%M%S')
    output_dir.mkdir(parents=True, exist_ok=True)

    # ファイルパスの取得(コマンドライン引数 or ダイアログ)
    if len(sys.argv) > 1:
        # コマンドライン引数またはドラッグ&ドロップから取得
        file_paths = [Path(arg) for arg in sys.argv[1:] if Path(arg).exists()]
        if not file_paths:
            print("エラー: 有効なファイルパスが指定されていません")
            input("Enterキーを押して終了...")
            return
    else:
        # ダイアログで複数ファイル選択
        paths = fd.askopenfilenames(filetypes=[("JSON/JSONL files", "*.json;*.jsonl")])
        if not paths:
            return
        file_paths = [Path(p) for p in paths]

    # 各ファイルを処理
    for file_path in file_paths:
        print(f"\n処理中: {file_path.name}")

        if file_path.suffix == '.jsonl':
            process_jsonl_file(file_path, output_dir)
        elif file_path.suffix == '.json':
            process_json_file(file_path, output_dir)
        else:
            print(f"スキップ: 未対応のファイル形式 ({file_path.name})")

    print(f"\n全ての処理が完了しました")
    print(f"出力先: {output_dir}")
    input("\nEnterキーを押して終了...")


if __name__ == "__main__":
    main()

exporter.bat(Windows用)

@echo off
python "%~dp0exporter.py" %*
pause

説明:

  • %~dp0: バッチファイルのあるフォルダパス(末尾に\付き)
  • %*: ドラッグ&ドロップされたすべてのファイルパス
  • pause: 実行後にウィンドウを閉じずに結果を確認できる

コードの詳細解説

1. JSONL処理のコア部分

def process_jsonl_file(file_path, output_dir):
    messages = []
    session_id = None

    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            # 各行をJSONとしてパース
            entry = json.loads(line)

ポイント:

  • JSONL形式は1行=1JSONオブジェクト
  • json.loads()で各行を個別にパース
  • セッション全体で必要な情報を蓄積

2. 賢いフィルタリング

# システムメッセージを除外
if entry.get('isMeta', False):
    continue

# ツール実行結果を除外
if isinstance(content, list):
    if any(item.get('type') == 'tool_result' for item in content):
        continue

効果:

  • 人間とアシスタントの会話のみを抽出
  • ファイル編集やコマンド実行の中間結果をスキップ
  • 読みやすい会話ログを生成

3. システムタグの除去

def clean_system_tags(text):
    """Claude Codeのシステムタグを除去"""
    paired_tags = [
        r'.*?',
        r'.*?',
        # ... その他のタグ
    ]

    for pattern in paired_tags:
        text = re.sub(pattern, '', text, flags=re.DOTALL)

    return text.strip()

必要性:

  • Claude Codeのログには などのシステムタグが含まれる
  • これらは内部処理用の情報で、会話ログには不要
  • 正規表現で自動除去することで、純粋な会話内容だけを抽出

4. 配列形式のコンテンツ処理

if isinstance(content, list):
    # textタイプのコンテンツのみ抽出(user/assistant共通)
    text_parts = [item.get('text', '') for item in content if item.get('type') == 'text']
    content = '\n\n'.join(text_parts)

# システムタグを除去
content = clean_system_tags(content) if isinstance(content, str) else content

Claude Codeの特性:

  • user/assistantともに配列形式でメッセージが格納されることがある
  • ツール使用の説明と結果が分かれて格納される
  • textタイプのみを抽出して結合することで、会話部分を取得
  • などのシステムタグを自動除去

4. セッション識別

# セッションID取得
if session_id is None and 'sessionId' in entry:
    session_id = entry['sessionId']

# フォルダ名とセッションIDで識別
folder_name = file_path.parent.name
title = f"chat_{session_id[:8]}"
filename = f"{safe_folder_name}_{safe_title}.md"

実用的な識別:

  • セッションIDだけでは識別困難
  • フォルダ名(プロジェクト名など)を含めることで識別性向上
  • 先頭8文字のみ使用して簡潔に

5. 複数ファイル処理

if len(sys.argv) > 1:
    # コマンドライン引数またはドラッグ&ドロップ
    file_paths = [Path(arg) for arg in sys.argv[1:] if Path(arg).exists()]
else:
    # ダイアログで複数選択
    paths = fd.askopenfilenames(filetypes=[("JSON/JSONL files", "*.json;*.jsonl")])

利便性:

  • 複数の入力方法に対応
  • ドラッグ&ドロップで直感的に処理
  • スクリプト化も可能

出力例

入力: transcript.jsonl(Claude Codeセッション)

{"type":"user","sessionId":"abc123def456","message":{"role":"user","content":"Pythonで素数判定関数を作成して"}}
{"type":"assistant","sessionId":"abc123def456","message":{"role":"assistant","content":[{"type":"text","text":"素数判定関数を作成します..."}]}}

出力: my-project_chat_abc12345.md

# my-project_chat_abc12345

**[human]**:

Pythonで素数判定関数を作成して

**[assistant]**:

素数判定関数を作成します...

実際の使用場面

1. プロジェクト開発の記録

# プロジェクトフォルダのセッションを一括エクスポート
cd C:\Users\user\.claude\sessions
python exporter.py my-web-app\transcript.jsonl api-server\transcript.jsonl

活用方法:

  • プロジェクト毎の開発履歴を整理
  • 設計判断の理由を後から確認
  • 新メンバーへのオンボーディング資料

2. トラブルシューティングの記録

エラー解決のセッションを保存:

python exporter.py bug-fix-session\transcript.jsonl

活用方法:

  • 同じエラーが再発した時の解決策を即座に参照
  • チーム内でトラブル事例を共有
  • ナレッジベースに追加

3. 学習記録

新しい技術を学んだセッションを整理:

# Claude Codeで学習したセッションをまとめてエクスポート
python exporter.py react-learning\transcript.jsonl typescript-basics\transcript.jsonl

活用方法:

  • 学習過程を振り返る
  • 理解した内容を復習
  • ブログ記事の素材

4. 週次レビュー

1週間のセッションを一括処理:

# PowerShellで過去7日間のセッションを抽出
$sessions = Get-ChildItem -Path "C:\Users\user\.claude\sessions\*\transcript.jsonl" -Recurse |
            Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-7) }
python exporter.py $sessions.FullName

活用方法:

  • 週の振り返りMTG資料
  • 進捗報告の補助資料
  • 学んだことの整理

5. バックアップと保存

定期的なバックアップスクリプト:

# バッチファイル例(backup_sessions.bat)
@echo off
set OUTPUT_DIR=D:\claude_backups\%date:~0,4%%date:~5,2%%date:~8,2%
mkdir %OUTPUT_DIR%
python exporter.py C:\Users\user\.claude\sessions\*\transcript.jsonl
move output\* %OUTPUT_DIR%

応用アイデア

1. 自動分類スクリプト

# タグ別にフォルダ分け
import shutil

tag_keywords = {
    'Python': ['python', 'django', 'flask'],
    'JavaScript': ['javascript', 'react', 'node'],
    'Database': ['sql', 'postgres', 'mysql'],
}

for md_file in output_dir.glob('*.md'):
    content = md_file.read_text(encoding='utf-8')

    for tag, keywords in tag_keywords.items():
        if any(kw in content.lower() for kw in keywords):
            tag_dir = output_dir / tag
            tag_dir.mkdir(exist_ok=True)
            shutil.move(md_file, tag_dir / md_file.name)
            break

2. 統計情報の抽出

# セッションの統計
def analyze_session(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    human_msgs = content.count('**[human]**')
    assistant_msgs = content.count('**[assistant]**')
    total_chars = len(content)

    print(f"{file_path.name}:")
    print(f"  人間: {human_msgs}回, Claude: {assistant_msgs}回")
    print(f"  総文字数: {total_chars}")

3. Obsidian/Notion連携

Markdown出力なので、そのままObsidianやNotionにインポート可能:

# Obsidian vaultにコピー
cp output/20260215_100000/*.md ~/ObsidianVault/Claude-Sessions/

4. 全文検索インデックス

# 簡易検索機能
import glob

def search_sessions(keyword, output_dir):
    results = []
    for md_file in Path(output_dir).glob('**/*.md'):
        content = md_file.read_text(encoding='utf-8')
        if keyword.lower() in content.lower():
            results.append(md_file)
    return results

# 使用例
found = search_sessions('素数判定', 'output/20260215_100000')
for f in found:
    print(f"見つかりました: {f.name}")

トラブルシューティング

エラー1: JSONDecodeError

原因: JSONL形式が壊れている(途中で保存が中断された等)

解決方法:

# スクリプトは自動でスキップするため、通常は問題なし
except json.JSONDecodeError:
    continue  # 壊れた行はスキップ

エラー2: セッションIDが見つからない

症状: ファイル名が <フォルダ名>_transcript.md になる

原因: セッションIDが記録されていない(古い形式等)

対処: ファイル名(transcript)で識別されるため問題なし

エラー3: メッセージが含まれていません

原因: ツール実行結果のみでユーザー/アシスタントの会話がない

対処: これは正常な動作(処理がスキップされる)

エラー4: UnicodeDecodeError

原因: 特殊な文字が含まれている

解決方法:

# open時にerrors='ignore'を追加
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:

まとめ

この拡張版Claude Chat Exporterを使えば:

  • ✅ Claude Codeの全セッションを一括でMarkdown化
  • ✅ 読みやすい形式で長期保存・検索可能に
  • ✅ プロジェクトの開発履歴として資産化
  • ✅ チーム内でのナレッジ共有が簡単に
  • ✅ 従来のWebチャットにも引き続き対応

Claude Codeを使えば使うほど、貴重な知的財産が蓄積されます。このツールで効率的に管理・活用して、開発の生産性を更に高めましょう。

Amazon検索[本 Python]

関連記事

以前のバージョン(Webチャット専用)の解説記事:

Claudeチャットを資産化!エクスポートデータを整形するPythonツール
Claudeとの会話履歴は貴重な知的財産です。しかし、エクスポートしたJSONファイルは読みにくく、そのままでは活用しづらいのが現状です。この記事では、ClaudeのエクスポートデータをMarkdown形式に整形し、チャット毎に分割して保存...

GitHubでの管理

このツールをGitHubで管理する方法:

cd C:\my-local\my-python\claude_chat_exporter

# .gitignoreを作成
cat > .gitignore << 'EOF'
# 出力ファイル
output/
*.json
*.jsonl

# Python
__pycache__/
*.pyc

# エディタ
.vscode/
.idea/
EOF

# Git初期化
git init
git add exporter.py .gitignore
git commit -m "feat: Claude Code session exporter"

# GitHubにプッシュ(GitHub CLI使用)
gh repo create claude-chat-exporter --private --source=. --push

GitHub連携の詳細は以下の記事を参照:

GitHub CLI完全ガイド:コマンドラインでGitHub操作を効率化
GitHub CLI(ghコマンド)は、GitHubの操作をコマンドラインで完結できる公式ツールです。ブラウザを開かずに、リポジトリ作成からIssue管理、Pull Request作成まで、すべてターミナルで行えます。この記事では、実際の導...

ご意見・改善提案があれば、ぜひコメントでお知らせください!

コメント