先週の朝、いつものように git のログを眺めていて、手が止まりました。ある壁紙アプリのリポジトリに、前夜のジョブが書いたはずの変更履歴が入っていて、そこに古いバージョン番号が紛れ込んでいたのです。
問題そのものは小さなものです。厄介だったのは「どの実行がこれを書いたのか」がまるで分からなかったことでした。私自身、いくつかの個人開発アプリのリポジトリに対して、依存更新・変更履歴の下書き・Crashlytics のクラッシュ要約・AdMob メディエーション設定の点検といった細かな作業を、深夜に Claude Code のヘッドレス実行でまとめて走らせています。多い夜は12セッション。セッションを識別する手がかりは不透明な UUID だけで、コミットメッセージにも実行の身元は残っていませんでした。
結局、ログのタイムスタンプとコミットの時刻を突き合わせて犯人の実行を特定するのに、15分ほどかかりました。原因の修正よりも、原因の実行を探すほうが長かったのです。
Claude Code に読みやすいセッション名が入ったことで、この状況は設計しなおせます。鍵は「名前を、意味を持った一本の糸にする」ことでした。
名前を相関キーにするという考え方
セッション名を単なる表示上の飾りとして扱うと、翌朝には何の助けにもなりません。私が変えたのは、名前を 相関キー(correlation key) として設計したことです。相関キーとは、あとから別々の記録同士を突き合わせるための、共通の見出しのことです。
私が採用したキーの形はこうです。
| 要素 | 例 | 役割 |
| アプリ名 | wallpaper-zen | どのリポジトリの作業か |
| タスク種別 | changelog-draft | 何をする実行か |
| JST 日付 | 20260706 | いつの夜間バッチか |
| 短いランダム値 | a1b2c3d4 | 同分実行の衝突回避 |
つなぐと wallpaper-zen-changelog-draft-20260706-a1b2c3d4 になります。この一本の文字列を、セッション名・コミット・ログファイルの三か所すべてに同じ形で刻む。それだけで、どこか一点から残り二点へ辿れるようになります。追跡が考古学ではなく grep になる、という設計です。
実行ラッパーで相関キーを組み立てる
まず、夜間ジョブを1本起動するたびに相関キーを生成し、セッション名として渡すラッパーを用意します。
#!/usr/bin/env bash
# run-nightly.sh — 1つの夜間ジョブを可読な相関キー付きで実行する
set -euo pipefail
APP="$1" # 例: wallpaper-zen
TASK="$2" # 例: changelog-draft
# 相関キー = アプリ-タスク-JST日付-短いランダム値
# タイムゾーンは必ず明示する。素の date は UTC 基準で、深夜の実行だと前日にずれる
DATE_JST="$(TZ=Asia/Tokyo date +%Y%m%d)"
SHORT="$(head -c4 /dev/urandom | od -An -tx1 | tr -d ' \n')"
export CC_SESSION_NAME="${APP}-${TASK}-${DATE_JST}-${SHORT}"
LOG_DIR="$HOME/nightly-logs/${DATE_JST}"
mkdir -p "$LOG_DIR"
LOG="${LOG_DIR}/${CC_SESSION_NAME}.log"
echo "[$(TZ=Asia/Tokyo date +%H:%M:%S)] start ${CC_SESSION_NAME}" | tee -a "$LOG"
# Claude Code をヘッドレスで実行し、可読なセッション名としてこのキーを渡す。
# セッション名を指定できるオプションに CC_SESSION_NAME を渡すのが要点です。
# もしお使いのバージョンでセッション名を直接指定できなくても、
# 後述のコミットトレーラーとログ側にキーを刻めば相関は完成します。
claude -p "$(cat "prompts/${TASK}.md")" \
--session-name "$CC_SESSION_NAME" \
2>&1 | tee -a "$LOG"
echo "[$(TZ=Asia/Tokyo date +%H:%M:%S)] done ${CC_SESSION_NAME}" | tee -a "$LOG"
ここでの肝は二つあります。ひとつは、ログのファイル名そのものを相関キーにしていること。もうひとつは、CC_SESSION_NAME を環境変数として渡していることです。この環境変数が、次のコミットフックへの橋渡しになります。
コミットに相関キーを刻む
Claude Code がリポジトリ内でコミットを作るとき、そのコミットメッセージに相関キーを自動で添えたい。これは prepare-commit-msg フックで実現できます。
#!/usr/bin/env bash
# .git/hooks/prepare-commit-msg
# 環境変数 CC_SESSION_NAME があれば、コミットメッセージ末尾にトレーラーとして刻む
set -euo pipefail
MSG_FILE="$1"
[ -n "${CC_SESSION_NAME:-}" ] || exit 0
# 既に同じトレーラーがあれば二重に書かない(べき等にする)
if grep -q "^Session-Name: ${CC_SESSION_NAME}$" "$MSG_FILE"; then
exit 0
fi
printf '\nSession-Name: %s\n' "$CC_SESSION_NAME" >> "$MSG_FILE"
Session-Name: という行は、Git のトレーラー(Signed-off-by: などと同じ末尾メタデータの仕組み)として解釈されます。本文とは分けて機械的に取り出せるのが利点です。
コミットメッセージは、Before と After でこう変わります。
Before:
Update changelog for release
After:
Update changelog for 2.4.0 release
Session-Name: wallpaper-zen-changelog-draft-20260706-a1b2c3d4
一行増えただけに見えますが、この一行が翌朝の15分を消してくれます。
コミットからログへ一気に辿る
相関キーが三か所に揃ったので、あとは辿るだけです。コミットの SHA を渡すと、トレーラーから相関キーを取り出し、対応するログの末尾を見せるスクリプトを用意します。
#!/usr/bin/env bash
# trace.sh <commit-sha> — コミットからセッション、ログへ辿る
set -euo pipefail
SHA="$1"
# トレーラーから相関キーを取り出す
KEY="$(git log -1 --format='%(trailers:key=Session-Name,valueonly)' "$SHA" | head -1)"
if [ -z "$KEY" ]; then
echo "このコミットにはセッション名トレーラーがありません: $SHA"
exit 1
fi
# キーに含まれる 8 桁の日付を取り出してログの場所を決める
# アプリ名やタスク名にハイフンが含まれても壊れないよう、日付だけを正規表現で拾う
DATE_JST="$(printf '%s' "$KEY" | grep -oE '[0-9]{8}' | head -1)"
LOG="$HOME/nightly-logs/${DATE_JST}/${KEY}.log"
echo "セッション名: $KEY"
echo "ログ: $LOG"
echo "----"
if [ -f "$LOG" ]; then
tail -n 20 "$LOG"
else
echo "ログが見つかりません(保管期間切れの可能性があります)"
fi
trace.sh a3f9c21 と打てば、そのコミットを書いた実行のログ末尾が即座に出ます。逆向きも同じ相関キーで辿れます。ログを見て怪しい実行を見つけたら、git log --grep="Session-Name: <キー>" でそのセッションが触ったコミット群を引けます。片方向ではなく双方向に行き来できるのが、相関キーを共通の見出しにする効き目です。
どれだけ短くなったか
導入前後で、実際に「不審なコミットの発生源を特定するまでの時間」を三度計ってみました。
| 状況 | 導入前 | 導入後 |
| 手順 | ログの時刻とコミット時刻を目視で突き合わせ | trace.sh <sha> を1回 |
| 平均所要時間 | 約15分 | 20秒台 |
| 誤特定の起きやすさ | 同分に複数実行があると混同 | ランダム値で一意に確定 |
12セッションが同じ深夜帯に走る運用では、時刻だけの突き合わせは実質あてになりません。相関キーを一本通しただけで、朝の調査は「探す作業」から「開く作業」に変わりました。
つまずいたところ
素直に効いた設計ですが、二か所で確実にハマりました。
ひとつめはタイムゾーンです。最初は横着して date +%Y%m%d と書いていて、23時台の実行だけキーの日付が翌日にずれ、ログのフォルダが二つに割れました。夜間の無人実行では、日付を扱う箇所は必ず TZ=Asia/Tokyo を明示する。これは相関キーに限らず、ログの日付でも同じ罠を踏んだことがあるので、私はもう反射的に付けるようにしています。
ふたつめはキーの衝突です。アプリ名とタスク名と日付だけでキーを組んでいた頃、同じアプリの同じタスクを手動で追試したら、夜間分とファイル名がぶつかってログを上書きしかけました。末尾の短いランダム値は飾りではなく、同分・同条件の実行を確実に分けるための保険です。4バイトでも実用上は十分でした。
もうひとつ細かい注意として、prepare-commit-msg フックはコミットが実際に作られる前に走るので、--no-verify を付けて回す別のスクリプトがあるとトレーラーが抜けます。無人運用側では --no-verify を使わない、と決めておくのが安全です。
どの方式を選ぶか
相関の刻み先には、コミットトレーラー方式のほかにも選択肢があります。コミット本文に直書きする、別のメタデータファイルに書き出す、といった方法です。私はトレーラー方式を好みます。理由は、本文の可読性を損なわずに機械的な抽出ができること、そして git log の標準機能だけで双方向に辿れることです。外部ツールもデータベースも要りません。
一方で、コミットを一切作らない点検系のタスク(設定を検査して結果を通知するだけの類い)では、コミットが存在しないのでトレーラーは使えません。この場合は、通知メッセージの先頭に相関キーを一行入れておくと、通知からログへ辿れます。刻む先は違っても、「同じキーを複数の記録に通す」という原則は変わりません。
読みやすいセッション名という機能は、それ単体では画面上で少し便利になるだけです。けれど、その名前を自分の運用の相関キーとして設計し直すと、無人実行の追跡可能性そのものが変わります。私にとっては、夜間ジョブを安心して任せられるかどうかを分ける、地味だけれど確かな一歩でした。
まず試すなら、今夜の実行ログのファイル名を相関キーに変え、prepare-commit-msg フックを一つ置くところから始めてみてください。それだけで、次に「どの実行がこれを書いたのか」と迷ったとき、答えは grep 一回で返ってきます。
最後までお読みいただき、ありがとうございました。私自身まだ運用を磨いている途中ですが、無人実行の透明性について、共に少しずつ良くしていけたら嬉しいです。