この記事でできること
個人開発している技術記事キュレーションサービス「DevPick」と、その姉妹サービス「AgentPick」では、どちらもClaude Codeでの作業にgit worktreeを多用しています。
Claude Codeのgit push/gh pr create前にdocs整合性チェックやAIコードレビューを走らせるフックを組んでいると、git worktreeで複数セッションを並行させたときに、フックが元のリポジトリ本体の差分を見てしまい、無関係な変更でブロックされたり、逆に本来レビューすべき変更を素通りさせたりすることがあります。原因は環境変数CLAUDE_PROJECT_DIRをディレクトリ特定に使っていることです。この記事では、フック入力JSONのcwdを使う直し方をまとめます。設計の背景・気づいた経緯はZenn版で書いています。
前提
- Claude Codeの
.claude/settings.jsonで、git push/gh pr createをトリガーにしたPreToolUseフックを組んでいる -
EnterWorktreeなどでgit worktreeのセッションに入り、そこからpush/PR作成を行う運用をしている
この2つを組み合わせている場合に、以下の問題が起こり得ます。
何が起きるか
Claude CodeのセッションでCLAUDE_PROJECT_DIRを使ってリポジトリのルートにcdしているフックは、次のように書かれていることが多いはずです。
cd "${CLAUDE_PROJECT_DIR:-.}"
CLAUDE_PROJECT_DIRはセッション開始時点の値で固定される仕様で、セッション中にEnterWorktreeでworktree配下に移動しても値は更新されません。Bashツールの実際のカレントディレクトリ(pwd)はworktree配下に切り替わっているのに、フック側はセッション開始時のリポジトリ本体を見続けます。
この状態でworktreeからpushすると、フックは実際にはworktreeではなくリポジトリ本体の方でgit diffを取ります。この時リポジトリ本体側に別の未pushの変更が残っていれば、それがworktree側の差分として誤って扱われ、無関係な差分でブロックされます。逆にリポジトリ本体側がクリーンなタイミングでは、空diffとして扱われ、本来レビューすべきworktree側の変更がノーチェックで通ってしまいます。
直し方:フック入力JSONのcwdを優先する
Claude Codeはフックを実行するたびに、標準入力にJSON形式のコンテキストを渡します。このJSONには、Bashツールが実際に呼び出された時点のカレントディレクトリがcwdフィールドとして含まれています。CLAUDE_PROJECT_DIRより、こちらが実態を正しく反映しています。
cdする前に、標準入力からこのcwdを読み取るようにします。
input="$(cat 2>/dev/null || true)"
target_cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
cd "${target_cwd:-${CLAUDE_PROJECT_DIR:-.}}" 2>/dev/null || exit 0
ポイントは3つです。
- 標準入力は一度しか読めないので、
catで変数に受けてからjqで.cwdを取り出す(パイプで直接jqに渡すと、後続処理で入力を再利用できなくなる) -
.cwdが無い場合に備えて// emptyで空文字にフォールバックさせ、jqの評価自体が失敗してスクリプトを止めないようにする - 最終的な
cd先はcwd→CLAUDE_PROJECT_DIR→.の優先順位にする。worktreeを使わない通常セッションではcwdとCLAUDE_PROJECT_DIRは基本的に一致するので、この変更で既存の動作は壊れない
settings.json側のインラインフックも同じ対応が必要
.claude/settings.jsonにインラインで書いているフック(例えばpush前チェックスクリプトを呼ぶだけの短いコマンド)も、同じ理由でworktree配下では誤動作します。こちらも標準入力からcwdを取り出し、チェックスクリプトの実行場所として使うようにします。
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
case "$cmd" in
*"gh pr create"*|*"git push"*)
target_cwd=$(printf '%s' "$input" | jq -r '.cwd // empty')
cd "${target_cwd:-$CLAUDE_PROJECT_DIR}" 2>/dev/null && bash "$CLAUDE_PROJECT_DIR/scripts/pre-push-check.sh"
;;
esac
チェックスクリプト自体の置き場所($CLAUDE_PROJECT_DIR/scripts/...)はリポジトリ本体で固定でよいですが、そのスクリプトが何を対象にチェックするか(cd先)はcwd優先にする必要がある、という点は同じです。
まとめ
-
CLAUDE_PROJECT_DIRはセッション開始時に固定され、EnterWorktree後も値が変わらない - push前フックのディレクトリ特定にこれだけを使っていると、worktreeセッションで無関係な差分を見てブロックされたり、逆に本来の変更がノーチェックで通ったりする
- フック入力JSON(標準入力)の
cwdフィールドには、実際に呼び出された時点のカレントディレクトリが入っている。これをCLAUDE_PROJECT_DIRより優先して使う -
cwdが取れない場合だけCLAUDE_PROJECT_DIRにフォールバックすれば、worktreeを使わない既存の運用は壊さずに済む
worktreeで複数セッションを並行させる運用をしている場合は、pushやPR作成の前に自動で走らせているフック・スクリプトがディレクトリ特定に何を使っているか、一度洗い出しておくことをおすすめします。