@kentaro さんの記事「会話を自動でObsidianに記録する仕組みを作った 」を見て、Claude Codeとの会話を自動でObsidianに記録する仕組みを導入してみました。
非常に便利な仕組みですが、実際に動かすまでにいくつかハマったポイントがあったので、解決策とともに共有します。
元記事の仕組み(概要)
Claude Code セッション
↓ (jsonlファイルに記録)
~/.claude/projects/*/session.jsonl
↓ (5秒ごとに監視)
watch-and-save.sh (LaunchAgentで常駐)
↓ (変更検知時に抽出・整形)
~/obsidian/claude/YYYY-M-D.md
発生した問題と解決策
問題1: jqがインストールされていない
元記事のスクリプトはjq(JSONパーサー)を使用しますが、macOSにはデフォルトでインストールされていません。
# 確認
which jq
# → jq not found
# 解決策
brew install jq
問題2: plistで~が展開されない
元記事のLaunchAgent設定:
<string>~/.claude/hooks/watch-and-save.sh</string>
LaunchAgentのplistでは~が展開されないため、絶対パスに変更する必要があります。
<!-- 修正後 -->
<string>/Users/[ユーザー名]/.claude/hooks/watch-and-save.sh</string>
<string>/Users/[ユーザー名]/.claude/logs/obsidian-sync.log</string>
<string>/Users/[ユーザー名]/.claude/logs/obsidian-sync-error.log</string>
問題3: ユーザーメッセージが配列の場合に対応していない
これが最大のハマりポイントでした。
Claude Codeのセッションファイルでは、ユーザーメッセージの.message.contentが文字列の場合と配列の場合があります。
// 文字列の場合(元スクリプトが想定)
{"type": "user", "message": {"content": "こんにちは"}}
// 配列の場合(ツール実行結果など)
{"type": "user", "message": {"content": [{"type": "text", "text": "こんにちは"}]}}
元スクリプトのjqフィルタは文字列のみを想定していたため、配列の場合にエラーが発生していました。
問題4: 新しいセッションで内容が記録されない
セッションヘッダーは書き込まれるのに、会話内容が空という症状が発生しました。
原因: 新しいセッション検出時に、過去の追跡ファイル(last_line_file)の値を使っていたため、「新しい行がない」と判定されていました。
問題5: LaunchAgent環境でjqのPATHが通らない
最もハマりやすい問題です。セットアップ直後は動くのに、Mac再起動後に会話内容が記録されなくなるという症状が発生しました。
原因: LaunchAgentのデフォルトPATHは /usr/bin:/bin:/usr/sbin:/sbin のみです。Homebrewでインストールしたjqは /usr/local/bin/jq(Intel Mac)または /opt/homebrew/bin/jq(Apple Silicon)に配置されますが、このパスはLaunchAgentのPATHに含まれていません。
初回セットアップ時はシェルのPATH設定が引き継がれて動作しますが、Mac再起動後にLaunchAgentが再開されるとPATHがデフォルトに戻り、jqが見つからなくなります。
スクリプト内で2>/dev/nullとしているとエラーが握り潰され、原因の特定が非常に困難になります。
# 確認方法:LaunchAgent環境をシミュレートしてjqが見つかるか確認
env -i PATH="/usr/bin:/bin:/usr/sbin:/sbin" bash -c 'which jq'
# → jq not found ← これが原因
# 解決策:スクリプトの先頭でPATHを追加
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
修正版スクリプト
上記の問題をすべて解決し、さらにセッション区切り機能を追加した修正版スクリプトです。
~/.claude/hooks/watch-and-save.sh:
#!/bin/bash
# Watch Claude Code session and sync to Obsidian in real-time (append mode)
# Ensure Homebrew tools (jq etc.) are accessible in LaunchAgent environment
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
OBSIDIAN_DIR="$HOME/Dropbox/Obsidian/claude" # ← 自分のObsidian vaultに変更
SESSION_DIR="$HOME/.claude/projects"
LAST_LINE_FILE="$HOME/.claude/last-synced-line"
LAST_SESSION_FILE="$HOME/.claude/last-session-file"
LOG_FILE="$HOME/.claude/watch-and-save.log"
mkdir -p "$OBSIDIAN_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
# Truncate string to specified character length (handles multibyte)
truncate_string() {
local str="$1"
local max_len="${2:-30}"
str=$(echo "$str" | tr 'n' ' ' | sed 's/ */ /g')
echo "$str" | awk -v len="$max_len" '{
if (length($0) > len) {
print substr($0, 1, len) "..."
} else {
print $0
}
}'
}
# Get first user message from session file
get_first_user_message() {
local session_file="$1"
head -100 "$session_file" | jq -r '
select(.type == "user") |
if (.message.content | type) == "array" then
(.message.content | map(select(.type == "text") | .text) | join(" "))
elif (.message.content | type) == "string" then
.message.content
elif (.content | type) == "string" then
.content
else
""
end
' 2>>"$LOG_FILE" | grep -v '^___PRE_BLOCK_6___#39; | head -1
}
sync_session() {
local session_file="$1"
local TODAY=$(date +%Y-%-m-%-d)
local OUTPUT_FILE="${OBSIDIAN_DIR}/${TODAY}.md"
local CURRENT_TIME=$(date '+%H:%M:%S')
# Create file with header if it doesn't exist
if [ ! -f "$OUTPUT_FILE" ]; then
echo "# ${TODAY} Claudeとの会話" > "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
fi
# Check if this is a new session
local last_session=""
if [ -f "$LAST_SESSION_FILE" ]; then
last_session=$(cat "$LAST_SESSION_FILE" 2>/dev/null || echo "")
fi
local is_new_session=false
if [ "$session_file" != "$last_session" ]; then
is_new_session=true
fi
# Get last synced line number for this specific session file
local session_hash=$(echo "$session_file" | md5 -q | cut -c1-8)
local last_line_file="${LAST_LINE_FILE}_${session_hash}"
local last_line=0
# Only use saved line number if this is NOT a new session
if [ "$is_new_session" = false ] && [ -f "$last_line_file" ]; then
last_line=$(cat "$last_line_file" 2>/dev/null || echo 0)
fi
# Count current lines in session file
local current_lines=$(wc -l < "$session_file" | tr -d ' ')
# Only process new lines
if [ "$current_lines" -gt "$last_line" ]; then
# Check if session changed - add header
if [ "$session_file" != "$last_session" ]; then
local first_msg=$(get_first_user_message "$session_file")
local truncated_msg=$(truncate_string "$first_msg" 30)
echo "" >> "$OUTPUT_FILE"
echo "---" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "## ${TODAY} ${CURRENT_TIME} ${truncated_msg}" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "$session_file" > "$LAST_SESSION_FILE"
log "New session detected: $session_file"
fi
local new_content=$(tail -n +$((last_line + 1)) "$session_file" | jq -r '
select(.type == "user" or .type == "assistant") |
if .type == "user" then
# Handle both string and array content
(if (.message.content | type) == "array" then
(.message.content | map(select(.type == "text") | .text) | join(" "))
elif (.message.content | type) == "string" then
.message.content
elif (.content | type) == "string" then
.content
else
""
end) as $content |
if ($content | length) > 0 then
if ($content | test("<local-command|<command-name>|<system-reminder>|<task-notification>"; "i")) then
empty
else
"<strong>ユーザー</strong>: " + $content
end
else
empty
end
elif .type == "assistant" then
if (.message.content | type) == "array" then
(.message.content[] | select(.type == "text") |
if (.text | test("^No response requested"; "i")) then
empty
else
"<strong>Claude</strong>: " + .text
end
)
else
empty
end
else
empty
end
' 2>>"$LOG_FILE")
if [ -n "$new_content" ]; then
echo "$new_content" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
log "Synced $((current_lines - last_line)) new lines to $OUTPUT_FILE"
# コンテンツ抽出成功時のみ行カウンタを更新(失敗時はリトライ可能にする)
echo "$current_lines" > "$last_line_file"
fi
fi
}
find_session() {
find "$SESSION_DIR" -path "*/subagents/*" -prune -o
-name "*.jsonl" -type f -mmin -60 -size +1000c -print
2>/dev/null | xargs ls -t 2>/dev/null | head -1
}
echo "Watching for Claude session changes (append mode)..."
log "Started watching for Claude session changes"
while true; do
SESSION=$(find_session) || true
if [ -n "$SESSION" ]; then
sync_session "$SESSION" || log "Error syncing session: $SESSION"
fi
sleep 5
done
修正版LaunchAgent
~/Library/LaunchAgents/com.claude.obsidian-sync.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.claude.obsidian-sync</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/[ユーザー名]/.claude/hooks/watch-and-save.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/[ユーザー名]/.claude/logs/obsidian-sync.log</string>
<key>StandardErrorPath</key>
<string>/Users/[ユーザー名]/.claude/logs/obsidian-sync-error.log</string>
</dict>
</plist>
注意: /Users/[ユーザー名]/ は自分のホームディレクトリに置き換えてください。
セットアップ手順
# 1. jqをインストール
brew install jq
# 2. ディレクトリ作成
mkdir -p ~/.claude/hooks
mkdir -p ~/.claude/logs
# 3. スクリプトを配置(上記の内容を保存)
nano ~/.claude/hooks/watch-and-save.sh
# 4. 実行権限を付与
chmod +x ~/.claude/hooks/watch-and-save.sh
# 5. plistを配置(上記の内容を保存、パスを自分のものに変更)
nano ~/Library/LaunchAgents/com.claude.obsidian-sync.plist
# 6. LaunchAgentを読み込み
launchctl load ~/Library/LaunchAgents/com.claude.obsidian-sync.plist
# 7. 動作確認
launchctl list | grep claude
# → PIDが表示されればOK
出力例
修正版では、セッションごとに区切りが入ります:
# 2026-1-27 Claudeとの会話
---
## 2026-1-27 12:01:55 watch-and-save.sh が正常に動きそう...
<strong>ユーザー</strong>: /Users/sara-hina/.claude/hooks/watch-and-save.sh が正常に動きそうかチェックしてもらえますか。
<strong>Claude</strong>: スクリプトを確認しました。いくつか問題点と改善提案があります。
...
---
## 2026-1-27 14:30:22 マニュアル生成を開始してください
<strong>ユーザー</strong>: マニュアル生成を開始してください
<strong>Claude</strong>: マニュアル生成を開始します。
...
元記事との主な違い
| 項目 | 元記事 | 修正版 |
|---|---|---|
| ユーザーメッセージ | 文字列のみ対応 | 配列にも対応 |
| セッション区切り | なし | ---と見出しで区切り |
| ログ機能 | なし | ~/.claude/watch-and-save.log |
| 新セッション処理 | 過去の行数を引き継ぐ | 0から開始 |
| plistパス | ~使用 |
絶対パス |
| jqのPATH | 未設定 | /usr/local/bin等を明示的に追加 |
| エラーログ | 2>/dev/nullで破棄 |
ログファイルに記録 |
| 行カウンタ | 常に更新 | コンテンツ抽出成功時のみ更新 |
トラブルシューティング
ログが記録されない場合
# エラーログを確認
cat ~/.claude/logs/obsidian-sync-error.log
# 通常ログを確認
cat ~/.claude/watch-and-save.log
# セッションファイルが見つかるか確認
find ~/.claude/projects -name "*.jsonl" -type f -mmin -60 -size +1000c 2>/dev/null | head -3
LaunchAgentを再起動する
launchctl unload ~/Library/LaunchAgents/com.claude.obsidian-sync.plist
launchctl load ~/Library/LaunchAgents/com.claude.obsidian-sync.plist
同期位置をリセットする
rm -f ~/.claude/last-synced-line_*
Mac再起動後にログが記録されなくなった場合
jqがLaunchAgent環境で見つからない可能性があります。
# LaunchAgent環境をシミュレートしてjqの可否を確認
env -i PATH="/usr/bin:/bin:/usr/sbin:/sbin" bash -c 'which jq'
# 「jq not found」と出た場合、スクリプト先頭に以下があるか確認
# export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
# jqの実際のパスを確認
which jq
# → /usr/local/bin/jq(Intel)または /opt/homebrew/bin/jq(Apple Silicon)
まとめ
実際に動かすにはいくつかの修正が必要でした。
主なポイント:
- jqのインストールを忘れずに
- plistは絶対パスで指定
- 配列形式のメッセージに対応
- 新しいセッションは0行目から開始
- スクリプト内でPATHを明示的に設定(LaunchAgent環境ではHomebrewのパスが通らない)
これでClaude Codeとの会話が自動でObsidianに蓄積されるようになりました。過去の会話を検索・参照できるのは非常に便利です。
元記事を書いてくださった@kentaroさんに感謝します!
関連記事
Claude CodeにSlash Commandsで、開発日誌を書いてもらう方法

