3行まとめ
- Claude Codeの会話ログはJSONL形式で保存されており、DuckDBを使って日次の利用状況や音声入力の課題などを分析できる
- 英語プロンプトの学習効率化やエラーパターンの特定など、自分の仕事の仕方を改善するための実践的な活用方法がある
- JSONLファイルのスキーマ情報を整理することで、Claude Codeがクエリを書く際の精度が向上する
はじめに
Claude Codeは非常に強力なツールで、これ自体は別のブログで書く予定ですが、もはやこれなしでコードを書けないほど便利に使っています。今回は、そのClaude Codeとの会話ログを分析することで、自分の仕事の仕方を改善する方法について紹介します。これだけClaude Codeを使っているため、データを扱う職種としては、自分がどのようなやり取りをClaude Codeと行っているのかを知りたくなります。
Claude Codeのログ保存機能とその特徴
Claude Codeはコマンドラインツールということもあり、ローカルに自分がどのような発話をして、エージェントがどのような内容を出力したのかがJSONL(JSON Lines:1行に1つのJSONオブジェクトを記述する形式)のファイルで保存されています。
Claude Codeとの会話のログは、JSONL形式で保存されますが、Claude Codeのドキュメントには構造が示されていないため、どのようなスキーマでJSONLログが出力されるかが明確ではありません。このため、人間にとってもClaude Codeにとっても分かりにくく、筆者も最初は理解に苦労し、Claude Codeも適切なスキーマ情報なしでは分析クエリを間違えることがありました。
ログ分析の活用例
この情報の活用例は以下のとおりです。
- どのようなプロジェクト、リポジトリでどれくらいの会話をしていたのかを日ごとに分析する
- 音声入力特有の課題を把握する
音声入力の課題と英語プロンプトの活用
音声入力については、私はこれを介してClaude Codeとのプロンプトを書いていますが、音声入力は特に日本語の場合、同音異義語の問題があるため正しく認識されない場合があります。
しかし、エージェントは音声認識の誤りも適切に処理してくれるため、ほとんど問題にはなりませんが、多少間違えてやり直しをしなければならないこともあります。そのような場合、英語でプロンプトを話した方が早いこともありますが、私はそれほど英語が流暢でありません。
例えば、プロンプトで実際によく使う表現を覚えることで、汎用的な英語学習(ネイティブスピーカーとの日常会話や一般的な雑談のための英語学習)よりも効率的に、エージェントとのやり取りに特化したプロンプト用の英語を身につけられるのではないかと思っています。
DuckDBを用いた分析アプローチ
Claude Codeとの会話頻度や使用パターンを把握するために、例えば日ごとにどのような作業をどれくらい行っていたかを知ろうとした時に、Claude Codeに毎回集計を依頼するのは負荷が大きいため、統計・集計処理が必要になります。
例えばJSONファイルであればjqでやってもよいんですが、jqはレコード単位・フィルタリング向きであり、DuckDBは集計関数やウィンドウ関数を要する統計解析向きであるため、こういった統計処理にはDuckDBで行うのが非常に有用です。DuckDBの使い方については、過去に書いた記事があるため、それを参照してください。
実際にどのようなSQLを書けばよいのかは、以下のような事例があります。
分析の起点となるSQL(クリックで開きます)
SELECT sessionId, timestamp, cwd, type as message_type, -- メッセージタイプ: 'user' または 'assistant' message.role as message_role, -- メッセージロール: 'user' または 'assistant' -- プロジェクト名をcwdパスから抽出(cwdは常に存在する) SPLIT_PART(cwd, '/', -1) as project_name, -- === メッセージコンテンツの抽出 === -- message.contentフィールドには異なるデータ構造が含まれる: -- 1. プレーン文字列: ユーザーの直接入力テキスト(ユーザーメッセージの20%) -- 2. textタイプのJSON配列: [{"type":"text","text":"content"}](ユーザーメッセージの3%) -- 3. tool_resultタイプのJSON配列: [{"type":"tool_result","content":"result"}](ユーザーメッセージの73%) -- 4. tool_useタイプのJSON配列: [{"type":"tool_use","name":"tool","input":{...}}](主にassistant) -- 5. thinkingタイプのJSON配列: [{"type":"thinking","thinking":"thoughts"}](主にassistant) -- 6. 混合コンテンツ: 配列内の複数要素(稀だが重要) -- メッセージ構造に基づいて抽出される主要なテキストコンテンツ CASE WHEN json_type(message.content) = 'VARCHAR' THEN message.content::VARCHAR -- プレーン文字列メッセージ(ユーザーの直接入力) WHEN json_extract_string(message.content, '$[0].type') = 'text' THEN json_extract_string(message.content, '$[0].text') -- JSON配列からのテキストコンテンツ WHEN json_extract_string(message.content, '$[0].type') = 'thinking' THEN json_extract_string(message.content, '$[0].thinking') -- Claudeの内部思考プロセス WHEN json_extract_string(message.content, '$[0].type') = 'tool_result' THEN LEFT(json_extract_string(message.content, '$[0].content'), 500) -- ツール実行結果(切り捨て) WHEN json_extract_string(message.content, '$[0].type') = 'tool_use' THEN CONCAT('Tool: ', json_extract_string(message.content, '$[0].name'), ' | Input: ', LEFT(json_extract(message.content, '$[0].input')::VARCHAR, 200)) -- ツール使用情報 ELSE 'Complex/Unknown content structure' END as message_text, -- === 追加のメッセージメタデータ === json_type(message.content) as content_data_type, -- JSONタイプ: VARCHAR, ARRAY, OBJECT等 json_extract_string(message.content, '$[0].type') as content_element_type, -- 配列内の最初の要素タイプ CASE WHEN json_type(message.content) = 'ARRAY' THEN json_array_length(message.content) ELSE NULL END as content_array_length, -- 複雑なメッセージの配列長 -- ツール固有の情報抽出 CASE WHEN json_extract_string(message.content, '$[0].type') = 'tool_use' THEN json_extract_string(message.content, '$[0].name') ELSE NULL END as tool_name, -- 使用されているツール名 CASE WHEN json_extract_string(message.content, '$[0].type') = 'tool_result' THEN json_extract_string(message.content, '$[0].tool_use_id') ELSE NULL END as tool_use_id, -- ツール結果とツール使用を紐付けるID CASE WHEN json_extract_string(message.content, '$[0].type') = 'tool_result' THEN json_extract_string(message.content, '$[0].is_error') ELSE NULL END as is_tool_error, -- ツール実行にエラーがあったかどうか -- === 追加のメッセージメタデータ(messageオブジェクト全体から) === message.id as message_id, -- メッセージ一意識別子(assistantのみ) message.model as claude_model, -- 使用Claudeモデル(assistantのみ) message.stop_reason as stop_reason, -- 応答停止理由(assistantのみ) message.stop_sequence as stop_sequence, -- 停止シーケンス(通常null) -- === トークン使用量情報(assistantのみ) === json_extract(message, '$.usage.input_tokens')::INT as input_tokens, -- 入力トークン数 json_extract(message, '$.usage.output_tokens')::INT as output_tokens, -- 出力トークン数 json_extract(message, '$.usage.cache_creation_input_tokens')::INT as cache_creation_tokens, -- キャッシュ作成トークン数 json_extract(message, '$.usage.cache_read_input_tokens')::INT as cache_read_tokens, -- キャッシュ読み取りトークン数 -- === コスト計算用の派生カラム === (COALESCE(json_extract(message, '$.usage.input_tokens')::INT, 0) + COALESCE(json_extract(message, '$.usage.output_tokens')::INT, 0)) as total_tokens -- 総トークン数 FROM read_json_auto( '/Users/yasuhisa.yoshida/.claude/projects/*/*.jsonl', format='newline_delimited', ignore_errors=true, union_by_name=true ) WHERE type = 'user' AND message.content IS NOT NULL AND message.content::VARCHAR NOT LIKE '%<command-name>%' AND message.content::VARCHAR NOT LIKE '%Caveat:%' AND message.content::VARCHAR NOT LIKE '%<local-command-stdout>%' AND message.content::VARCHAR NOT LIKE '%[Request interrupted%';
そうすると、例えば以下のような結果が返ってきます。
分析結果の例(クリックで開きます)
sessionId | timestamp | cwd | message_type | message_role | project_name | message_text | content_data_type | content_element_type | content_array_length | tool_name | tool_use_id | is_tool_error | message_id | claude_model | stop_reason | stop_sequence | input_tokens | output_tokens | cache_creation_tokens | cache_read_tokens | total_tokens |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7ce95790-df99-40d4-9202-59ada1345e96 | 2025-06-13T13:51:37.613Z | /Users/yasuhisa.yoshida/hoge | user | user | auto_register_schema | """このスキーマは今手動で登録してるけど、これを自動で発見する方法を考えて。""" | VARCHAR | 0 | |||||||||||||
7ce95790-df99-40d4-9202-59ada1345e96 | 2025-06-13T13:51:49.721Z | /Users/yasuhisa.yoshida/fuga | user | user | auto_register_schema | """この関数の引数にtype annotationを付与して""" | VARCHAR | 0 | |||||||||||||
7ce95790-df99-40d4-9202-59ada1345e96 | 2025-06-13T13:51:55.094Z | /Users/yasuhisa.yoshida/piyo | user | user | auto_register_schema | "Found 1 file /Users/yasuhisa.yoshida/piyo/src/schema_registry.py" | ARRAY | tool_result | 1 | toolu_01C9gYoJQLb27ES4VouKyDej | 0 |
スキーマ情報の重要性とログ分析の活用
このような情報をClaude Codeに「このようなことをやってほしい」とお願いすることもできますが、そのJSONLがどのようなスキーマになっているかを把握してもらった方が、実際の作業が進めやすくなります。具体的なSQLとスキーマ情報を組み合わせることで、Claude Codeが実際のプロンプトを解釈して分析することが容易になると考えられます。
そのようなことを考えて、実際にClaude Codeの会話ログを見てもらった上で、どのようなスキーマになっているかという内容自体をClaude Codeに出力させた実際のスキーマ例は以下のとおりです。
Claude Codeとの会話ログのスキーマ(クリックで開きます)
{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://claude-code-logs.schema.json", "title": "Claude Code Conversation Log Schema", "description": "Claude Code会話ログファイル(JSON-L形式)の完全なスキーマ定義。12,901件のユーザーメッセージと詳細分析に基づく。", // === 全体構造 === // Claude Codeログは JSON-L(改行区切りJSON)形式で保存される // 各行が独立したメッセージエントリを表す // 保存場所: ~/.claude/projects/*/conversations/*.jsonl "type": "object", "required": ["type", "sessionId", "timestamp", "cwd", "message"], "additionalProperties": false, "properties": { "type": { "type": "string", "enum": ["user", "assistant"], "description": "メッセージの送信者タイプ", // 実際の分布: user 47% (12,901件), assistant 53% (14,633件) // 用途: ログ分析時のロール別フィルタリング、統計作成 // 注意: message.roleと同じ値だが、トップレベルでのクイックアクセス用 }, "sessionId": { "type": "string", "pattern": "^session_[a-zA-Z0-9_-]+$", "description": "セッションの一意識別子", // 用途: 会話の流れを追跡、セッション別分析、関連メッセージのグループ化 // 形式: "session_" + ランダム文字列 // 重要: 同一セッション内でuser/assistantが交互に出現するのが正常 }, "timestamp": { "type": "string", "format": "date-time", "description": "メッセージ作成時刻(ISO 8601形式)", // 例: "2025-06-15T10:30:45.123Z" // 用途: 時系列分析、セッション時間計算、パフォーマンス分析 }, "cwd": { "type": "string", "description": "現在の作業ディレクトリ(絶対パス)", // 重要: 常に存在する(NULL値は発見されず) // 用途: プロジェクト識別(SPLIT_PART(cwd, '/', -1)でプロジェクト名抽出) // 例: "/Users/yasuhisa.yoshida/work/transcript-refiner" // → プロジェクト名: "transcript-refiner" }, "message": { "type": "object", "description": "メッセージの実際の内容とメタデータ", // このオブジェクトの構造はuser/assistantで大きく異なる "oneOf": [ { // === USER MESSAGE STRUCTURE === "$comment": "ユーザーメッセージ: ユーザー入力またはツール実行結果", "required": ["role", "content"], "additionalProperties": false, "properties": { "role": { "type": "string", "const": "user", "description": "メッセージロール(常に'user')" }, "content": { "description": "ユーザーメッセージのコンテンツ", // === ユーザーメッセージの実際のパターン === // 73%: [{"type":"tool_result","content":"..."}] - ツール実行結果 // 20%: "直接入力文字列" - ユーザーの直接プロンプト // 3%: [{"type":"text","text":"..."}] - 構造化テキスト // 4%: その他(エラーメッセージ、コマンド出力等) "oneOf": [ { // プレーン文字列: ユーザーの直接入力(20%) "type": "string", "minLength": 1, "description": "ユーザーの直接プロンプト入力", // 例: "manually_mapping_schema これを自動で発見する方法を考えて" // 用途: プロンプト分析、ユーザー意図解析 }, { // 配列形式: 構造化コンテンツ(80%) "type": "array", "minItems": 1, "maxItems": 2, // 実際の最大値は2(image + text) "description": "構造化されたメッセージコンテンツ", "items": { "oneOf": [ { // tool_result: ツール実行結果(73%のケース) "type": "object", "required": ["type", "tool_use_id", "content"], "additionalProperties": false, "properties": { "type": { "const": "tool_result", "description": "ツール実行結果タイプ" }, "tool_use_id": { "type": "string", "pattern": "^toolu_[a-zA-Z0-9]+$", "description": "対応するtool_useのID", // 例: "toolu_01GFj7Vp4VkQjjaAm" // 用途: tool_useとtool_resultの対応関係追跡 }, "content": { "description": "ツール実行の結果", // content自体も多様な形式を取る "oneOf": [ { "type": "string", "description": "プレーンテキスト結果" // 例: ファイル内容、コマンド出力、エラーメッセージ }, { "type": "array", "description": "構造化された結果(特に画像)", "items": { "type": "object", "properties": { "type": { "enum": ["image", "text"], "description": "結果コンテンツのタイプ" } } } } ] }, "is_error": { "type": ["boolean", "string"], "description": "ツール実行エラーフラグ", // 実際の値: true, false, "true", "false", または未定義 // 用途: エラー率分析、デバッグ } } }, { // text: 構造化テキスト(3%のケース) "type": "object", "required": ["type", "text"], "additionalProperties": false, "properties": { "type": { "const": "text", "description": "テキストコンテンツタイプ" }, "text": { "type": "string", "description": "テキスト内容", // 例: セッション継続メッセージ、システムメッセージ } } }, { // image: 画像コンテンツ(稀) "type": "object", "required": ["type", "source"], "additionalProperties": false, "properties": { "type": { "const": "image", "description": "画像コンテンツタイプ" }, "source": { "type": "object", "required": ["type", "data"], "properties": { "type": { "const": "base64", "description": "画像データ形式" }, "data": { "type": "string", "description": "Base64エンコードされた画像データ", // 非常に大きなサイズ(平均527KB) // 用途: スクリーンショット、ファイル画像の送信 } } } } } ] } } ] }, // ユーザーメッセージには以下のフィールドは存在しない "id": { "type": "null", "description": "ユーザーメッセージには常にnull" }, "model": { "type": "null", "description": "ユーザーメッセージには常にnull" }, "stop_reason": { "type": "null", "description": "ユーザーメッセージには常にnull" }, "stop_sequence": { "type": "null", "description": "ユーザーメッセージには常にnull" }, "usage": { "type": "null", "description": "ユーザーメッセージには常にnull" } } }, { // === ASSISTANT MESSAGE STRUCTURE === "$comment": "アシスタントメッセージ: Claudeの応答、ツール使用、思考プロセス", "required": ["role", "content", "id", "model", "stop_reason", "usage"], "additionalProperties": false, "properties": { "role": { "type": "string", "const": "assistant", "description": "メッセージロール(常に'assistant')" }, "id": { "type": "string", "pattern": "^msg_[a-zA-Z0-9]+$", "description": "メッセージの一意識別子", // 例: "msg_01SQHfrz7QUJxw..." // 用途: メッセージ間の依存関係追跡、会話フロー分析 }, "model": { "type": "string", "enum": [ "claude-sonnet-4-20250514", "claude-opus-4-2025013", "<synthetic>" ], "description": "使用されたClaudeモデル", // 実際の分布: // - claude-sonnet-4-20250514: 主要使用(154,537入力 + 2,062,719出力トークン) // - claude-opus-4-2025013: 補助使用(19,551入力 + 736,236出力トークン) // - <synthetic>: 特殊ケース(0トークン) // 用途: モデル別性能分析、コスト計算 }, "content": { "type": "array", "minItems": 1, "maxItems": 9, // 実際の最大値は9 "description": "アシスタントメッセージのコンテンツ配列", // アシスタントのcontentは常に配列形式 "items": { "oneOf": [ { // text: 通常のテキスト応答 "type": "object", "required": ["type", "text"], "additionalProperties": false, "properties": { "type": { "const": "text", "description": "テキスト応答タイプ" }, "text": { "type": "string", "minLength": 1, "description": "Claudeの応答テキスト", // 平均長: 約1,200文字 // 用途: 主要な応答内容、回答品質分析 } } }, { // tool_use: ツール使用指示 "type": "object", "required": ["type", "id", "name", "input"], "additionalProperties": false, "properties": { "type": { "const": "tool_use", "description": "ツール使用タイプ" }, "id": { "type": "string", "pattern": "^toolu_[a-zA-Z0-9]+$", "description": "ツール使用の一意識別子", // tool_resultのtool_use_idと対応 }, "name": { "type": "string", "enum": [ "Bash", "Edit", "Read", "TodoWrite", "Write", "Glob", "LS", "WebSearch", "WebFetch", "Grep", "Task", "MultiEdit", "exit_plan_mode", "NotebookRead", "NotebookEdit" ], "description": "使用するツール名", // 頻度順: Bash(469), Edit(156), Read(109), TodoWrite(101), Write(24)... // 用途: ツール使用パターン分析、ワークフロー分析 }, "input": { "type": "object", "description": "ツールへの入力パラメータ", // inputの構造はツールごとに異なる "oneOf": [ { // Bashツール "properties": { "command": {"type": "string"}, "description": {"type": "string"} }, "required": ["command", "description"] }, { // Readツール "properties": { "file_path": {"type": "string"}, "offset": {"type": "integer"}, "limit": {"type": "integer"} }, "required": ["file_path"] }, { // Editツール "properties": { "file_path": {"type": "string"}, "old_string": {"type": "string"}, "new_string": {"type": "string"}, "replace_all": {"type": "boolean"} }, "required": ["file_path", "old_string", "new_string"] }, { // その他のツール用の汎用構造 "additionalProperties": true } ] } } }, { // thinking: 内部思考プロセス "type": "object", "required": ["type", "thinking"], "additionalProperties": false, "properties": { "type": { "const": "thinking", "description": "思考プロセスタイプ" }, "thinking": { "type": "string", "description": "Claudeの内部思考内容", // 平均長: 約200文字 // 用途: 推論プロセス分析、デバッグ }, "source": { "type": "string", "description": "思考の出典(オプション)" } } } ] } }, "stop_reason": { "type": "string", "enum": ["tool_use", "end_turn", "stop_sequence"], "description": "応答停止の理由", // 実際の分布: // - tool_use: 84.94% (4,208件) - ツール使用で停止 // - end_turn: 13.34% (661件) - 自然な応答終了 // - stop_sequence: 1.72% (85件) - 停止シーケンスで停止 // 用途: ツール使用パターン分析、会話完了検知 }, "stop_sequence": { "type": ["string", "null"], "description": "停止シーケンス文字列", // 通常はnull、stop_reason='stop_sequence'の場合のみ値を持つ }, "usage": { "type": "object", "required": ["input_tokens", "output_tokens"], "description": "トークン使用量情報(コスト計算に重要)", "properties": { "input_tokens": { "type": "integer", "minimum": 0, "description": "入力トークン数", // 平均: Sonnet-4で1,247トークン, Opus-4で1,956トークン // 用途: コスト計算、入力サイズ分析 }, "output_tokens": { "type": "integer", "minimum": 0, "description": "出力トークン数", // 平均: Sonnet-4で2,107トークン, Opus-4で2,848トークン // 用途: コスト計算、応答サイズ分析 }, "cache_creation_input_tokens": { "type": ["integer", "null"], "minimum": 0, "description": "キャッシュ作成用入力トークン数", // キャッシュ機能使用時のみ存在 // 用途: キャッシュ効率分析 }, "cache_read_input_tokens": { "type": ["integer", "null"], "minimum": 0, "description": "キャッシュ読み取り用入力トークン数", // キャッシュ機能使用時のみ存在 // 用途: キャッシュ効率分析、コスト最適化 } } } } } ] } }, // === 使用例とパターン === "examples": [ { // ユーザーの直接入力例 "type": "user", "sessionId": "session_abc123", "timestamp": "2025-06-15T10:30:45.123Z", "cwd": "/Users/yasuhisa.yoshida/work/transcript-refiner", "message": { "role": "user", "content": "manual_schema_mapping これを自動で発見する方法を考えて", "id": null, "model": null, "stop_reason": null, "stop_sequence": null, "usage": null } }, { // ツール実行結果例(最も一般的なパターン) "type": "user", "sessionId": "session_abc123", "timestamp": "2025-06-15T10:31:00.456Z", "cwd": "/Users/yasuhisa.yoshida/work/transcript-refiner", "message": { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01GFj7Vp4VkQjjaAm", "content": " 1\tdef _get_default_mapping():\n 2\t return {...}", "is_error": false } ] } }, { // アシスタントのツール使用例 "type": "assistant", "sessionId": "session_abc123", "timestamp": "2025-06-15T10:30:50.789Z", "cwd": "/Users/yasuhisa.yoshida/work/transcript-refiner", "message": { "role": "assistant", "id": "msg_01SQHfrz7QUJxw", "model": "claude-sonnet-4-20250514", "content": [ { "type": "text", "text": "まず現在の実装を確認しましょう。" }, { "type": "tool_use", "id": "toolu_01GFj7Vp4VkQjjaAm", "name": "Read", "input": { "file_path": "/path/to/file.py" } } ], "stop_reason": "tool_use", "stop_sequence": null, "usage": { "input_tokens": 1247, "output_tokens": 89, "cache_creation_input_tokens": null, "cache_read_input_tokens": 512 } } } ] }
実際のClaude Codeとの会話のログを分析することで、以下のような改善が可能になると考えています。
- 典型的なパターンをClaude Code自体に出してもらう
- 自分が英語で話したプロンプトの内容をClaude Codeに渡し、「この英語はこのように言った方がよい」と文法が少し間違っているため、このようにした方がよいということを後から振り返る
これは一例に過ぎませんが、そのような形でClaude Codeの会話ログは、自分の仕事の仕方を改善していくという意味で非常に役に立つものだと思っています。
ログの長期保存設定
なお、こういった分析を長期間にわたって行いたい場合は、Claude Codeの設定ファイルでcleanupPeriodDays
の値を調整することを推奨します。~/.claude/settings.json
にこの設定を追加することで、ログの保存期間を延長できます。
筆者は3年分(約1095日)の会話ログを保存するように設定しました。ディスク容量は現在の使用量から推測すると数ギガバイト程度になる可能性が高いですが、最近では数ギガバイトのディスク容量はそれほど大きな問題ではなく、こういった分析ができることの価値の方が重要だと考えています。
まとめ
このようなスキーマ情報や具体的な分析のSQLなどを活用しながら、今後Claude Codeとの仕事の仕方を改善していくやり方を模索したいと思っています。