Claude Codeにcdさせたらシェルがハングした話 → 犯人はシェルスナップショットでした
症状
Claude Codeに以下の構成のリポジトリで作業させていたところ、サブディレクトリ内のnpm run buildがハングしました。
project/
├── package.json
├── backend/
└── frontend/
└── package.json
# ルートで実行 → 正常に完了
npm run build
# サブディレクトリに移動して実行 → ハング
cd frontend && npm run build
なお自分で叩く分には問題なく成功します。Claude Code上でのみ発生する現象です。
他のコマンドも試してみましたが、どうもnpmやfrontendディレクトリがどうという話でなく、cd xxx && yyyが軒並みハングしているようです。
調査
ハング中のプロセスを確認すると、ezaがCPU使用率0%で停止していました。
ezaは.zshrcで以下のようにchpwdフックを定義し、cd実行時に自動でディレクトリ一覧を表示するようにしていました。
chpwd () {
if [[ $(pwd) != $HOME ]]; then
eza || ls
fi
}
しかしフックが定義されているとはいえ、.zshrcは非インタラクティブシェルでは読み込まれないはず。
実はClaude Codeはインタラクティブ実行していた……?いやそんなまさか。
原因
調べたところ、Claude Codeのシェル実行はシェルスナップショット(~/.claude/shell-snapshots/)を利用して、元のターミナル環境を再現?していることが分かりました。
~/.claude/shell-snapshots/の中身を見ると、4000行超の巨大なsnapshot-zsh-xxxxx.shが多数存在しました。
どうもインタラクティブシェルの状態が保存されているようです。上記のchpwdフックもこのスナップショット内に含まれていました。
なので真相としては
- Claude Codeの起動時だかシェル実行時だかに、このスナップショットが読み込まれる
-
cd実行 - 非インタラクティブシェルでありながら
chpwdフックが発火 -
ezaが呼び出される - 何らかの原因で処理がブロックされてハング
という流れのようでした(ブロックした直接的な原因は未特定)。
解決策
更に調査を進めたところ、Claude Codeはシェルスナップショット作成時にCLAUDECODEという環境変数を1に設定していることが分かりました。
これを.zshrcの冒頭でチェックし、早期リターンするように修正することで、インタラクティブ時専用の設定がスナップショットにキャプチャされないようにしました。
(念のため非インタラクティブシェルのチェックも追加しました)
# .zshrc の冒頭
# PATH など最低限必要な設定
export PATH="$HOME/.volta/bin:$PATH"
# Claude Code環境または非インタラクティブシェル環境では早期リターン
if [[ -n "$CLAUDECODE" || ! -o interactive ]]; then
return
fi
# 以下はインタラクティブシェル専用の設定
この修正により、Claude Code上でcd frontend && npm run buildを実行してもハングせず正常に完了するようになりました。
ちなみに、修正前後でスナップショットのサイズが大幅に減少しました (4538行 → 16行)。
まとめ
- Claude Codeはシェルスナップショットで環境を復元する
- スナップショットには
chpwdなどのシェル関数もキャプチャされる -
.zshrcでCLAUDECODE環境変数をチェックして早期リターンすることで回避可能
割と数日ハマっていたので解決できて良かったです……。
Discussion