Claude Codeでgit submoduleを更新する手順とdetached HEADの落とし穴
Claude Codeにsubmoduleを更新させるときの手順です。親リポジトリのポインタ更新とコミット、detached HEADで作業が消える条件、push順、--recurse-submodules、権限ルールの切り方をまとめます。
Claude Codeでgit submoduleを更新するときに押さえる点は1つです。submoduleの更新は、親リポジトリが記録している「コミットへのポインタ」を動かす操作で、親側でコミットして初めて完了します。サブディレクトリの中身が新しくなっただけでは、チームの誰にも反映されません。
この記事では、Claude Codeに更新を任せるときの手順を、つまずく順に並べます。ポインタのずれ、detached HEADで作業が消える条件、親とsubmoduleのpush順、--recurse-submodules の使いどころ、そして permissions の切り方です。
submoduleの更新は親リポジトリのポインタを動かす操作
submoduleは、親リポジトリの中に別のリポジトリを置く仕組みです。親が覚えているのは、submoduleの最新コミットではなく、特定の1コミットです。
git submodule update --remote を実行すると、submoduleの中でfetchが走り、リモートの最新コミットがチェックアウトされます。この時点で親リポジトリの git status には modified: <パス> (new commits) と出ます。まだコミットしていない、という合図です。
つまり更新には2段あります。
submodule更新の2段階
- 1
submoduleを新しいコミットへ動かす
git submodule update --remote <パス>で、submodule側の作業ツリーを更新します。 - 2
親リポジトリで新しいポインタをコミットする
git add <パス>とgit commitで、親が指すコミットを更新します。ここまでやって、他の人がcloneしたときに同じ版が出ます。
Pro Gitには、コミットするとsubmoduleは新しいコードに固定され、他の人が更新したときに同じコードを受け取る、という説明があります。逆に言えば、2段目を忘れた状態は「手元だけ新しい」状態です。
Claude Codeに更新を頼むときの指示と確認コマンド
Claude Codeには、2段目まで含めて頼むのが確実です。依頼文だけでなく、プロジェクトの CLAUDE.md に手順を書いておくと、毎回同じ流れになります。次は書き方の一例です。
## submoduleの更新
- 更新は `git submodule update --remote <パス>` で行う
- 更新後は親リポジトリで `git status` と `git diff --submodule` を実行し、
どのコミット範囲に動いたかを報告する
- 親リポジトリの `git add <パス>` とコミットまでをひとまとまりにする
- submodule内で自分がコミットした場合は、親より先にsubmoduleをpushする更新後の確認には、読み取りだけで済むコマンドが向いています。
git submodule update --remote libs/db
git status
git diff --submodulegit diff --submodule を使うと、ポインタが動いたコミット範囲の一覧が見られます。毎回 --submodule を付けたくない場合は、git config --global diff.submodule log で既定の表示形式にできます。git config status.submodulesummary 1 を設定すると、git status にもsubmoduleの変更の要約が出ます。
Claude Codeに「どのコミットからどのコミットへ動いたか」を要約させるときは、このdiffの出力を材料にさせます。コミットメッセージは更新の理由を書く欄なので、範囲の一覧をそのまま貼るより、変更の要点を1行添える形が読みやすくなります。
追跡するブランチを変えたいときは、.gitmodules にブランチを書きます。git config -f .gitmodules submodule.<名前>.branch stable を実行すると、--remote の更新先がそのブランチになります。-f .gitmodules を付けずに実行すると手元の設定だけが変わり、チームの他の人には伝わりません。
detached HEADでコミットが失われる条件
git submodule update で更新したsubmoduleは、ブランチ上ではなくdetached HEADの状態になります。作業ブランチがないため、ここでコミットすると、次の git submodule update で失われる可能性があります。
Claude Codeにsubmodule内のコードを直させるときは、先にブランチへ移ってからにします。
cd libs/db
git checkout -b fix/connection-retryその後で git submodule update --remote --merge か --rebase を使うと、上流の更新と手元の作業をつなげられます。この指定を忘れて git submodule update --remote だけを実行すると、submoduleはサーバー上の状態にそろい、再びdetached HEADになります。ブランチ自体は残っているので、submoduleに戻ってブランチをチェックアウトし直し、手動でmergeかrebaseをすれば作業は取り戻せます。
また、submodule内に未コミットの変更があるときは、git submodule update が変更を上書きしません。fetchは行われても、手元の変更は守られます。
Claude Code側の安全策
Claude Codeがworktreeを片付けるときも、submoduleの未保存の作業は見られています。
submoduleを考慮するClaude Codeの動作
worktree終了時の確認
対話セッションのworktreeを終了するとき、変更・未追跡ファイル・新しいコミットに加えて、チェックアウト済みsubmoduleの中の未コミット作業も確認されます。
自動クリーンアップの除外
サブエージェントやバックグラウンドセッションのworktreeを定期的に掃除する処理は、submoduleに変更や未追跡ファイルがあると削除しません。この確認にはv2.1.274以降が必要です。
検査できない場合
submoduleの状態を検査できないときは、自動で削除せず確認が入ります。
worktreeでsubmoduleを触る作業には、この安全側の動きが効きます。worktree自体の使い方はClaude Codeのworktree機能の解説で扱っています。
ポインタのずれが起きる3つの場面
「親が指すコミット」と「submoduleの実際のチェックアウト」がずれると、git status に modified: <パス> (new commits) が出ます。原因は主に3つです。
| 場面 | 状態 | 直し方 |
|---|---|---|
| 親側のpullやブランチ切り替えだけ行った | 状態親は新しいポインタ、submoduleは古いまま | 直し方git submodule update --init --recursive |
| submoduleを更新したが親でコミットしていない | 状態submoduleだけ新しい | 直し方親で git add <パス> とコミット |
| 別ブランチでポインタが違う | 状態ブランチごとに指すコミットが異なる | 直し方git checkout --recurse-submodules <ブランチ> |
1行目は、git pull の後にClaude Codeがビルドや型チェックを走らせると、古いsubmoduleのまま失敗する形で現れます。エラー文からは原因が見えにくいので、まず git status を見るよう指示しておくと切り分けが早くなります。
1行目の予防として、git pull --recurse-submodules(Git 2.14以降)が使えます。いつも付けるなら git config submodule.recurse true を設定します。この設定は --recurse-submodules を受け付ける各コマンドに効きますが、git clone は対象外です。
3行目の git checkout --recurse-submodules はGit 2.13以降で使えます。Pro Gitは、ブランチごとにsubmoduleが別のコミットを指すとき、常にこれを付けるのが無難だと述べています。
親とsubmoduleのpushの順番
submoduleの中でコミットして、親だけpushすると、他の人は親が指すコミットを取得できません。そのコミットはあなたの手元にしかないからです。
pushのときは、次の2つの方法で防げます。
git push --recurse-submodules=check: 未pushのsubmoduleコミットがあれば、pushを失敗させるgit push --recurse-submodules=on-demand: 必要なsubmoduleを先にpushしてから親をpushする
常にチェックさせたいなら、git config push.recurseSubmodules check で既定にできます。on-demand も同じキーで設定できます。
Claude Codeに「コミットしてpushして」と頼む前に、CLAUDE.md へ「submoduleに自分のコミットがあるときは --recurse-submodules=check を付ける」と書いておくと、順序の取り違えを機械的に止められます。Claude Codeでgit履歴から経緯を調べるで紹介したような履歴の調査と組み合わせると、どのコミットがどちらのリポジトリにあるかも追いやすくなります。
cloneで--recurse-submodulesを忘れたとき
submoduleを含むリポジトリを普通にcloneすると、submoduleのディレクトリは作られますが、中身は空です。git clone --recurse-submodules <URL> なら、ネストしたsubmoduleも含めて初期化と更新までされます。
忘れた場合は、後から次を実行します。
git submodule update --init --recursive--init は、新しいsubmoduleが追加されたpullの後にも必要です。--recursive は、submoduleがさらにsubmoduleを持つ場合に付けます。
submoduleのURLが変わったときは、git submodule sync --recursive でURLを手元の設定にコピーしてから、git submodule update --init --recursive を実行します。pullやupdateが「見つからないコミット」で失敗する典型は、このURL変更です。
CIでClaude Codeを動かす場合は、チェックアウトの段階でsubmoduleまで取得されているかを確かめます。取得されていなければ、上の git submodule update --init --recursive をワークフローの先頭で実行する手があります。全体の組み込みはClaude CodeをGitHub Actionsに組み込むにまとめています。
permissionsでsubmodule操作を切り分ける
submoduleの更新は、ファイルを書き換える操作と、ネットワークに出る操作が混ざっています。そこで permissions を、読み取りは許可、更新は確認、破壊的な操作は拒否の3段に分けると運用しやすくなります。次は設定の一例です。
{
"permissions": {
"allow": [
"Bash(git submodule status *)",
"Bash(git diff *)"
],
"ask": [
"Bash(git submodule update *)",
"Bash(git push *)"
],
"deny": [
"Bash(git submodule deinit *)"
]
}
}ルールの書き方には決まりがあります。ワイルドカードの * はサブコマンドの後ろに置きます。Bash(git submodule update *) は git submodule update 系だけに効き、Bash(git *) はすべてのgitコマンドを許可します。末尾の空白付き * は、引数なしのコマンドにも一致します。サブコマンドの前に * を置いたルールは、起動時に警告が出ます。
注意点が2つあります。
- ルールを書く位置: プロジェクトの
.claude/settings.jsonに書いたallowは、そのフォルダのワークスペース信頼ダイアログを承認した後でなければ適用されません。denyとaskは制限側なので、この影響を受けません - 信頼の範囲: gitリポジトリ内では、信頼はリポジトリのルートに結びつきます。信頼の対象にはリポジトリ全体が含まれますが、中にネストしたgitリポジトリ、たとえばsubmoduleは含まれません
2つ目は実務で効きます。親リポジトリを信頼しても、その信頼はsubmoduleに及びません。submoduleのディレクトリで直接 claude を起動する運用では、そこで改めて信頼の確認が入ることを見込んでおきます。同じ型の表示はClaude Codeのworkspace has not been trustedの原因でも説明しています。
hookやsandboxで気をつける点
PostToolUse hookは、Bashコマンドで変更されたファイルを tool_response.bashEditDiff で受け取れます(v2.1.269以降)。ただし一覧に入るのは、gitが無視しないファイルのうち、親リポジトリの下で変わったものです。submodule内のファイルは一覧に含まれません。submoduleの変更をhookで検出したい場合は、git status や git diff --submodule の出力を使うほうが確実です。
sandboxを有効にしている場合、作業ディレクトリの .git の中では hooks と config への書き込みが拒否されます。git config を書き換える手順をClaude Codeに任せるときは、この制限に当たる可能性があります。
プラグインのmarketplaceでも、submoduleは関係します。過去のリリースでは、marketplaceの更新がsubmoduleを同期しない問題や、submoduleを含むプラグインのインストールで初期化が不完全になる問題が修正されています。大きなsubmoduleを持つmarketplaceのリポジトリで、追加や更新が失敗する問題も修正済みです。
まとめ
Claude Codeにsubmoduleを任せるなら、手順は「更新して、diffで範囲を確認して、親でコミットする」の3つを1セットにします。作業ブランチを切らずにsubmodule内でコミットすると失われる場合があるので、コード変更を伴うときは先にブランチを作る、という一文を CLAUDE.md に残すのが最も効きます。
権限は、git submodule status と git diff を許可し、update と push は確認に回す構成から始められます。運用してみてsubmoduleの更新が頻繁なら、update を allow に移して、push だけを確認として残す形に調整できます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。