Claude Codeのログが静かに膨らむ問題を毎晩tail切り詰めで自己解決する
「Claude Code環境」シリーズです。前作「エージェントログをtrimする」でログ肥大に触れましたが、今回はその具体的な実装 ―― launchdで毎晩3:45に回るtail切り詰めスクリプトの話です。
~/.claude/logs/ は放っておくと静かに膨らみます。直近では agentmemory.err.log が1週間で5.3MBに達し、その前週は hook-latency.jsonl と vault-auto-ingest.log が同時に5MB超えで合計ディレクトリが20Mに届きました。監視スクリプトが tail -f でファイルを掴んでいるので、ファイルを新規作成するlogrotate方式は使えません。ファイルパスを変えずに中身だけ削る方式を選んだ理由がここにあります。
困りごと:logrotateはパスを変える
logrotate の標準動作は「古いファイルを .1 にリネームして新ファイルを作る」です。ファイルディスクリプタをinode経由で掴んでいる tail -f や inotifywait は追跡し続けますが、パスで読んでいるスクリプト(Claude Codeのステータスライン・cron・autopilot)は新ファイルを見に行かず、ログが空に見える状態になります。
毎回ローテーション後にスクリプト側のパス設定を書き換えるのは運用として続かない。だから「パスは固定、末尾だけ残す」方針にしました。
解決策:tail で末尾だけ残して mv 上書き
~/.claude/scripts/log-rotate.sh の核心がこの3行です。
MAX_BYTES=$((5 * 1024 * 1024)) # 5MB超えたら切詰め対象
KEEP_LINES=2000
tail -n "$KEEP_LINES" "$f" > "$f.tmp" && mv "$f.tmp" "$f"
statでバイト数を計測して5MB超のファイルだけを対象にするtail -n 2000で末尾2000行をテンポラリに書き出すmvでテンポラリを元ファイルに上書き → inodeは変わらず、パスも変わらず
> "$f" の直接リダイレクトではなく > "$f.tmp" && mv にしているのは、書き込み中のクラッシュでファイルが空になるのを防ぐためです。
logrotate -cのcopytruncateオプションでも「コピー後に元ファイルをtruncate」できますが、コピーと切り詰めの間にセッションが大量書き込みをすると最大で数秒分のログが消えます。tail + mvはその窓がない分、こちらのほうが安全です。
スクリプト全文
#!/bin/bash
# log-rotate.sh — ~/.claude/logs の肥大防止(毎日03:45 launchd)
set -uo pipefail
LOGDIR="$HOME/.claude/logs"
SELF_LOG="$LOGDIR/log-rotate.log"
MAX_BYTES=$((5 * 1024 * 1024))
KEEP_LINES=2000
echo "[$(date '+%F %T')] rotate start" >> "$SELF_LOG"
# 1) 肥大ログの切詰め(.log / .err / .out / .jsonl)
find "$LOGDIR" -maxdepth 1 -type f \( -name '*.log' -o -name '*.err' -o -name '*.out' -o -name '*.jsonl' \) | while read -r f; do
sz=$(stat -f%z "$f" 2>/dev/null || echo 0)
if [ "$sz" -gt "$MAX_BYTES" ]; then
tail -n "$KEEP_LINES" "$f" > "$f.tmp" && mv "$f.tmp" "$f"
echo "[$(date '+%F %T')] truncated $(basename "$f") ($sz bytes -> $(stat -f%z "$f") bytes)" >> "$SELF_LOG"
fi
done
# 2) 日付付き成果物・マーカー・バックアップの30日超削除
find "$LOGDIR" -maxdepth 1 -type f \( \
-name 'daily-brief-2*.md' -o -name 'project-health-2*.md' -o \
-name '.*-done-*' -o -name '.vault-ingest-*' -o -name '*.plist.bak*' \
\) -mtime +30 -delete 2>/dev/null
# 3) 自分自身のログも肥大防止
sz=$(stat -f%z "$SELF_LOG" 2>/dev/null || echo 0)
[ "$sz" -gt "$MAX_BYTES" ] && tail -n "$KEEP_LINES" "$SELF_LOG" > "$SELF_LOG.tmp" && mv "$SELF_LOG.tmp" "$SELF_LOG"
echo "[$(date '+%F %T')] rotate done (dir=$(du -sh "$LOGDIR" | cut -f1))" >> "$SELF_LOG"
-maxdepth 1 でサブディレクトリには降りず、~/.claude/logs/ 直下だけを対象にしています。サブディレクトリには別用途のプロジェクトデータが入っていることがあるため、誤削除を防ぐ保険です。
launchd で深夜静音実行
~/Library/LaunchAgents/com.shun.log-rotate.plist の該当部分です。
<key>LowPriorityIO</key>
<true/>
<key>Nice</key>
<integer>10</integer>
<key>ProcessType</key>
<string>Background</string>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>45</integer>
</dict>
</array>
3つの設定が「深夜静音実行」を作っています。
| キー | 値 | 効果 |
|---|---|---|
LowPriorityIO | true | ディスクI/Oの優先度を下げ、他プロセスに割り込まない |
Nice | 10 | CPU優先度を下げる(0が標準、正値ほど低優先) |
ProcessType | Background | macOSがバックグラウンド品質クラスで管理する |
StartCalendarInterval を3:45にしているのは、autopilotが5:00に起動するため、その直前にロールアップを終わらせておく意図です。Macがスリープ中のジョブはスキップされるため、深夜に画面を閉じているとその日は走りません。これは割り切っています。
pmset -g schedでスリープ解除スケジュールを組めますが、毎朝起こすと充電中でもバッテリーに悪影響が出ます。スリープ時スキップを許容してバッテリーを守る方を選びました。
30日超の dated artifact も削除する
ジョブは切り詰めと並行して、-mtime +30 の日付付きファイルを削除します。
find "$LOGDIR" -maxdepth 1 -type f \( \
-name 'daily-brief-2*.md' -o -name 'project-health-2*.md' -o \
-name '.*-done-*' -o -name '.vault-ingest-*' -o -name '*.plist.bak*' \
\) -mtime +30 -delete 2>/dev/null
daily-brief-2*.md や project-health-2*.md はClaude Codeの自動タスクが日次で吐く成果物です。1ファイル数KB〜数十KBでも、毎日積めば年間で数GBになります。30日を過ぎたら用済みとして消します。.*-done-* や .vault-ingest-* はcronジョブのチェックポイントファイルで、これも放置すると何百個にもなります。
実際の切り詰め記録
~/.claude/logs/log-rotate.log から直近の実績です。
[2026-07-19 03:45:05] truncated hook-latency.jsonl (5376787 bytes -> 178061 bytes)
[2026-07-19 03:45:05] truncated vault-auto-ingest.log (5255158 bytes -> 105277 bytes)
[2026-07-19 03:45:05] rotate done (dir=8.1M)
[2026-07-27 03:45:05] truncated agentmemory.err.log (5590723 bytes -> 297105 bytes)
[2026-07-27 03:45:06] rotate done (dir= 16M)
hook-latency.jsonl が5.1MB → 174KBに、vault-auto-ingest.log が5.0MB → 103KBに。2ファイルだけでその日のディレクトリが17M → 8.1Mに半減しています。直近2026-07-27の実行では agentmemory.err.log が5.3MBに達していたところを290KBまで落としました。
踏んだ落とし穴
> "$f"の直接リダイレクトで書き込み途中にクラッシュ →tmp経由のmvに変えて原子的にfindが.claude/配下を再帰してプロジェクトデータを削除しかけた →-maxdepth 1で直下のみに限定- launchd の最小 PATH でスクリプトが
statを見つけられなかった → plist のEnvironmentVariablesに/opt/homebrew/binを明示 - Macスリープ中にジョブがスキップされてログが何日も放置 → スキップを許容し、起きたタイミングで手動
launchctl kickstartを1コマンドにしておく - 自分自身の
log-rotate.logも肥大することに気づいた → スクリプト末尾でセルフ切り詰めを追加(スクリプト内に3セクション目として組み込み済み)
まとめ
~/.claude/logs/は.log/.err/.out/.jsonlが積み上がり、放置すると20M超になる- logrotate の新ファイル作成ではなく
tail -n 2000 > tmp && mv tmp $fで上書き → ファイルパスが変わらず監視スクリプトが壊れない - launchdの
LowPriorityIO + Nice=10 + ProcessType=Backgroundで深夜3:45に静かに走らせる - 30日超の
daily-brief-*.md等の dated artifact も同じジョブでfind -mtime +30 -deleteして片付ける
次回は、このログに記録が溜まったセッション履歴をどう長期記憶として再利用しているか ―― 会話ログを長期記憶に変える話 につながります。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!