AIエージェントのセッションIDをcommit messageに残すgit hookを作ってもらった

Claude CodeみたいなAIエージェントと開発していると、後から「これ何で実装したんだっけ」「当時どう指示したんだっけ」を掘り起こしたくなる事がある。
そこでcommit messageのfooterにセッションIDをtrailerとして自動で埋めるgit hookを作ってもらった。

やること
commit messageにこういうtrailerを1行足す。
Agent-Session: Claude Code on <hostname>: <session-id>
prepare-commit-msgフックで、環境変数 CLAUDE_CODE_SESSION_ID がある時だけ付与する。
人間が素の端末で打つcommitには付かない。

こうすると、 Agent-Session: Claude Code on 29-er: 73b4bffb-0cd2-4f22-8d0d-55a6fc087252 みたいなのができる。
これは実際にこれをつくってもらった時のsession id。

同じマシンなら claude --resume <id> で会話を再開できる。
セッションログはローカルにしか無いので、resumeは基本そのホスト限定。

ハマりどころ: core.hooksPathをグローバルに張ると危ない。
git config --global core.hooksPath ... で全repoに効かせると、gitは各repoの .git/hooks/ を一切見なくなる。
repo独自のpre-commit(lintやgofmt等)が黙って無効化される。
なので、グローバルフックから各repoの .git/hooks/<同名> を呼び直す委譲スクリプトを噛ませた。
委譲先の解決は git rev-parse --git-path hooks/X だと core.hooksPath 設定下でグローバル側を指してしまう。 --absolute-git-dir から組み立てるのが正解。

.gitconfig
Copied!
[core]
hooksPath = /Users/nana/.claude/git-hooks
こんな風に……。
_chain-to-local は、 /Users/nana/.claude/git-hooks 以下に applypatch-msg , commit-msg , post-checkout , post-commit , post-merge , pre-applypatch , pre-commit , pre-merge-commit , pre-push , pre-rebase についてそれを指すsymlinkをつくってもらった。

hook本体
prepare-commit-msg
Copied!
#!/bin/sh
# prepare-commit-msg: AIエージェントのセッションIDを commit message の trailer に付与する。
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"

host=$(hostname -s)

add_trailer() {
agent="$1"
id="$2"
# 既に同じIDが書かれていれば二重付与しない(amend / rebase 対策)
if grep -qF "$id" "$COMMIT_MSG_FILE" 2>/dev/null; then
return
fi
git interpret-trailers --in-place \
--trailer "Agent-Session=${agent} on ${host}: ${id}" "$COMMIT_MSG_FILE"
}

# merge / squash のメッセージには付けない(特定セッションの成果物ではないため)
case "$COMMIT_SOURCE" in
merge|squash) ;;
*)
[ -n "$CLAUDE_CODE_SESSION_ID" ] && add_trailer "Claude Code" "$CLAUDE_CODE_SESSION_ID"
;;
esac

# core.hooksPath で上書きされて動かなくなる repo 固有の prepare-commit-msg を補って呼ぶ。
# core.hooksPath 設定下では `--git-path hooks/X` がグローバル側を指すため、
# core.hooksPath の影響を受けない --absolute-git-dir から repo本来のパスを組む。
git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null)
repo_hook="$git_dir/hooks/prepare-commit-msg"
if [ -n "$git_dir" ] && [ -x "$repo_hook" ] && [ "$repo_hook" != "$0" ]; then
exec "$repo_hook" "$@"
fi

exit 0
case "$COMMIT_SOURCE" のところになんかうまいこと他のAIのsession idの分岐も仕込めば他のAIでも行けるはず。

各フック名から symlink して使う汎用の委譲役
_chain-to-local
Copied!
#!/bin/sh
# 汎用フック委譲役。
#
# グローバル core.hooksPath を張ると git は repo の .git/hooks/ を一切見なくなる。
# このスクリプトを各フック名にシンボリックリンクしておき、呼ばれたフック名と同名の
# repo固有フック (.git/hooks/<name>) があればそれを exec する。
# これで「グローバル設定」と「repo固有フック」を共存させ、後者が黙って無効化される
# 事故を防ぐ。

hook=$(basename "$0")
# 注意: core.hooksPath が設定されていると `git rev-parse --git-path hooks/X` は
# その設定先(=このグローバル側)を返してしまう。repo本来の .git/hooks/ を得るには
# core.hooksPath の影響を受けない --absolute-git-dir から組み立てる。
git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null)
repo_hook="$git_dir/hooks/$hook"

if [ -n "$git_dir" ] && [ -x "$repo_hook" ] && [ "$repo_hook" != "$0" ]; then
exec "$repo_hook" "$@"
fi

exit 0

---
nana AIによる っていってもかなり手で直してるんだけど……。

commit msgに刻むけど、他の人からは参照できない値なのでgit noteのほうが良いのではという説もある。