CTO室の高橋と申します。周囲からはニャンさんと呼ばれています。弊社ではエンジニア全員にClaude Codeが支給されており業務で利用することができます。とても便利なツールなのですが、絶賛開発途上の製品でもあり妙なトラブルに引っ掛かることも多々あります。今日はそんなClaude Code利用中に遭遇したトラブルの話をしようと思います。
以下の内容はClaude Code 2.1.74での状況で、新しいバージョンでは違う挙動になっているかもしれません。
lsでファイル一覧が得られない
Claude Codeはファイルの一覧を取得するためにlsコマンドを使用する。しかし私の環境ではlsの結果が空となってファイルのリストが得られないという問題に突き当たった。あるはずのファイルが見えないのでClaudeが混乱して無駄な調査を始めてしまった。一体何が起こっているのだろうか?
思い当たる節があり、Claudeにlsコマンドの実体を調べよと指示した。「ls: aliased to eza --time-style=iso 、lsはezaにエイリアスされています。これが原因です。ezaは非tty環境では結果を標準出力に書き出さないため△×※🐈(以下略」という回答が返ってきた。私のターミナル環境ではlsをezaというRust製のls代替ツールに置き換えており、Claude Codeのシェル環境にもそれが適用されているようだ。
おそらくezaの次のコードが原因。Claude Codeで実行すると!io::stdin().is_terminal()がtrueとなって、Stdinでパスを指定するモードになる。Stdinが空文字列であるため出力も空になっていたのだろう。これはfind ... | eza のようにパイプでパスを渡すことを想定した仕様と思われる。
impl FilesInput { pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> { Ok( if matches.has(&flags::STDIN)? || !io::stdin().is_terminal() { let separator = vars .get(EZA_STDIN_SEPARATOR) .unwrap_or(OsString::from("\n")); FilesInput::Stdin(separator) } else { FilesInput::Args }, ) } }
これで原因は確定。結論: ezaとClaude Codeの相性は悪い、併用している方は気をつけましょう おしまい
・・・・
・・・・
おしまい、ではない!Claudeはドヤ顔で調査結果を提示しているが、これはおかしい。
zshのエイリアスは~/.zshrcファイルで設定されている。~/.zshrcはzshをインタラクティブシェル(ターミナル上の対話モード)で起動した時に適用される設定ファイルだ。Claude Codeのコマンドは非インタラクティブシェルで実行される。このため~/.zshrcはロードされず、lsはezaにエイリアスされないはずだ。
Claude Codeにおける~/.zshrcロードの仕組み
Claude Codeはコマンドを実行するとき、ログインシェルかつ非インタラクティブシェルとしてzshを起動する。それらが何かという説明は省くが次の仕様を覚えていてほしい。
- ログインシェルとして起動:
~/.zprofileを読み込む - インタラクティブシェルとして起動:
~/.zshrcを読み込む
つまりClaude Codeのシェル環境には~/.zprofileだけが読み込まれるはずである。にもかかわらず~/.zshrcに書かれた設定が有効になっていた。これはどういう事だろうか?
調べてみると、Claude Codeは面白い仕組みで~/.zshrcを読み込んでいることが分かった。
まず各セッションでコマンドを最初に呼び出す時、~/.zshrcを読み込み、読み込んだ状態の環境変数、エイリアス、シェル関数などを ~/.claude/shell-snapshots/ 以下に保存する。そしてコマンドを実行する前にこのスナップショットをシェル環境にロードするのだ。この仕組みで非インタラクティブシェルにもかかわらず~/.zshrcで設定したエイリアスや環境変数が有効になる。
なぜ直接~/.zshrcをsourceしないのだろうか。これは最適化のためだろう。一般的に~/.zshrcにはClaude Codeには不要な設定(コマンド補完など)が多く記述されている。またnvmなどのツール類の初期化処理が含まれており時間がかかる。不要な設定の読み込みをキャンセルし、時間のかかる初期化処理をスキップするため直接~/.zshrcを読み込むのではなく、~/.zshrcを読み込んだ結果のみをキャッシュして再利用しているものと推測される。賢い仕組みである。
最終解決、$CLAUDECODE変数の利用
さて、ここまでわかれば後はClaudeにお任せだ。ターミナル起動時とClaude Code起動時で~/.zshrcの処理を分岐させるには?と尋ねると$CLAUDECODEという環境変数の存在を提示し、次のコード修正が提案された。普段はezaにエイリアスしつつ、Claudeはオリジナルのlsを使う。これで無事に動作するようになった。
# CLAUDE CODE以外でlsコマンドのエイリアスを設定 if [[ $CLAUDECODE != 1 ]]; then if command -v eza >/dev/null 2>&1; then alias ls=" eza --time-style=iso" alias ll=" eza -gl --time-style=long-iso" else alias ls=" /bin/ls -G" alias ll=" /bin/ls -lFG" fi fi
基本コマンドを置き換えると同様の不具合があるかもしれない。ls以外にも基本的なコマンドをエイリアスしているのであれば、それらも除外するのが無難だろう。