1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeのpush前フックをworktree対応にする(CLAUDE_PROJECT_DIRの罠)

1
Posted at

この記事でできること

個人開発している技術記事キュレーションサービス「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先はcwdCLAUDE_PROJECT_DIR.の優先順位にする。worktreeを使わない通常セッションではcwdCLAUDE_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作成の前に自動で走らせているフック・スクリプトがディレクトリ特定に何を使っているか、一度洗い出しておくことをおすすめします。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?