新しいworktreeを切って、さあnpm installを叩こうとしたところで詰まる。原因を切り分けようとして手順を変えて再実行すると、今度は違うエラーで止まる。同じ「worktreeを作ってnpm installする」というだけの作業なのに、毎回止まる場所が違う——sandbox化されたAIコーディングエージェント環境でfreshなworktreeにnpm installを回したことがある人なら、身に覚えがあるはずだ(唯一の救いは、犯人が毎回同じとは限らないので飽きないことくらいだ)。
これは1つのバグではなく、原因の異なる3つの障害が同じ症状(installが通らない)の顔をして出てくるパターンだ。$TMPDIRの解決タイミングのズレ、サンドボックスの書き込み許可リストの範囲外、npmキャッシュの権限崩れ——それぞれ原因のレイヤーがまったく違うので、1つの対処法を全部に当てはめようとすると空振りする。この記事では、エラーメッセージのどこを見ればどの問題を踏んでいるかを切り分け、読者が自分の環境で再現・検証できる手順とあわせて整理する。
この記事で学べること
- sandbox環境でgit worktreeにnpm installするとき踏みうる、原因の異なる3種類のエラーの見分け方
- それぞれのエラーメッセージの具体的な読み方と、再現・検証できるコマンド
- 3つの回避策と、どういう場面でどれを選ぶべきかの判断基準
前提条件
- Claude Code(Pro $20/月、Max $100〜200/月)のような、OSレベルでファイルシステムアクセスを制限するsandbox機構を使ってAIコーディングエージェントを動かしている
- git worktreeで複数のブランチ・タスクを並行して作業している
- fresh worktreeに対して
npm install(またはpnpm/yarn相当)を実行する運用がある
症状:同じ「npm installが通らない」でも中身が違う
まず、実際に起きる3つの症状を並べておく。どれも表面上は「作業が止まる」だけなので、初見では同じ問題に見えやすい。エラーメッセージだけを渡されて、この3つのうちどれを踏んでいるか言い当てられるだろうか。正直、自分も最初の1回目は当てられなかった(「さっきは通ったのに」と二度見しただけで終わった)。
症状A: git worktree add自体は成功する。ところがその直後に、作られたはずのディレクトリへcdしようとすると次のエラーで止まる。
cd: no such file or directory: /var/folders/xx/xxxxxxxx/T/my-worktree
症状B: ディレクトリへの移動はできる。npm installを実行すると、依存パッケージのダウンロードが始まる前の時点で止まる。
npm error code EPERM
npm error syscall mkdir
npm error path /var/folders/xx/xxxxxxxx/T/my-worktree/node_modules
npm error errno -1
npm error EPERM: operation not permitted, mkdir '/var/folders/xx/xxxxxxxx/T/my-worktree/node_modules'
症状C: worktree自体は許可された場所に作れていて、mkdirも通る。それでもnpm installが途中で止まる。
npm error code EPERM
npm error syscall rename
npm error path /Users/<username>/.npm/_cacache/tmp/xxxxxxxx
npm error errno -1
npm error EPERM: operation not permitted, rename '/Users/<username>/.npm/_cacache/tmp/xxxxxxxx' -> '/Users/<username>/.npm/_cacache/content-v2/xx/xx/xxxxxxxx'
3つとも「installが通らない」という一点では同じだが、エラーの出ている段階とパスがまったく違う。この違いこそが切り分けの手がかりになる。
| 症状 | エラーの出る段階 | エラーpathの特徴 | 原因 |
|---|---|---|---|
| A | cd(installより前) | worktreeのパスそのものが「無い」 | $TMPDIR解決不一致 |
| B | npm install直後(mkdir) | worktree配下のパス | 書き込み許可リスト外 |
| C | npm installの途中(rename) | ~/.npm/_cacache/配下のパス | npmキャッシュの権限崩れ |
切り分け方:エラーメッセージのどこを見るか
判断に迷ったときは、エラーがどのタイミングで・どのパスに対して出ているかの2点だけを見ればいい。
この3パターンは原因のレイヤーが完全に別物だ。原因1は環境変数の解決タイミングという非決定性の問題、原因2はファイルシステムのアクセス制御、原因3はキャッシュディレクトリの所有権崩れ。「とりあえずsandboxを再起動すれば直るだろう」と思って試すと、直る場合と直らない場合がある——期待と現実の差がまさにここに出る。1つの対処法を全部に当てはめようとした時点で(正直、一番時間を溶かすパターンだ)、遠回りの方向に舵を切っている。直前まで問題なく動いていたはずなのに、worktreeを切った瞬間だけ様子が変わる、という場面も珍しくない。
以前、Claude Codeのhookを入れて開発が止まったときの切り分け方を記事にしたことがある。あのときの教訓は「原因を1つに決め打ちせず、まず段階を分けて事象を特定する」だった。今回の3つのエラーも同じ発想で、どの段階でどんな見た目のエラーが出たかを先に固定してから対処に進むほうが、結果的に速い。
原因1: $TMPDIR解決タイミングの不一致によるcd失敗
sandbox環境では、$TMPDIRのようなOSの一時ディレクトリ変数が、コマンドの呼び出しごとに別々のタイミングで解決されることがある。たとえばgit worktree add "$TMPDIR/my-worktree"を実行した瞬間の$TMPDIRと、少し後でcd "$TMPDIR/my-worktree"を実行した瞬間の$TMPDIRが、同じ変数名でも別の値に展開されるケースがある(/var/folders/.../T/を指す場合と、別の一時領域を指す場合が混在するイメージだ)。
原因はシンプルで、パス「文字列」の一部としてではなく、変数$TMPDIRをそのつど展開して使い回しているところにある。1回目の展開で作られた実体のパスと、2回目の展開で参照しようとしたパスが一致しなければ、後者は文字通り「存在しないディレクトリ」を見に行くことになる。
sandbox対応が前提のツールほど、$TMPDIRを「そのつど解決される変数」ではなく「一度だけ解決して固定する値」として扱う設計にしているのは、このズレを踏まないための備えだといえる。
切り分けのポイント: git worktree addやmkdir -p自体は成功しているのに、後続のコマンドでcdやlsが「no such file or directory」で失敗する場合は、これを疑う。パスの綴りが合っているのに存在しないと言われる、というのがこの原因のサインだ。
原因2: 書き込み許可リスト外によるEPERM
sandbox機構の多くは、ファイルシステムへの書き込みを「許可リストに載っているパス配下だけ」に制限する。システムの$TMPDIR配下がその許可リストと完全一致しないケースがあり、その場合mkdirやnpm installが実行された瞬間にEPERM: operation not permittedで弾かれる。
このリポジトリでは以前、AIエージェントのネットワーク送信先をOSレベルで全拒否し、許可リストだけを明示的に通す設計を記事にしたことがある。ネットワークの送信先を絞る話と、ファイルシステムへの書き込みを絞る話は対象こそ違うが、考え方はまったく同じだ。「デフォルト全拒否+必要な範囲だけ明示的に許可する」という設計が、npmのようなツールチェーンが実際に触る先(レジストリのドメインだけでなく、書き込み先のディレクトリも含む)を漏れなく許可リストに載せていないと、正当な操作まで一緒に弾かれる。
{
"sandbox": {
"filesystem": {
"allowWrite": ["~/ghq/github.com/<org>/**"]
},
"network": {
"allowedDomains": ["registry.npmjs.org"]
}
}
}
上のような設定でallowWriteのパターンに$TMPDIR配下が含まれていなければ、$TMPDIRにworktreeを作ろうとした時点で書き込みが拒否される。ネットワーク側の許可(registry.npmjs.orgのようなレジストリのドメイン)が通っていても、ファイルシステム側の許可が別軸で必要になる点を見落としやすい。
切り分けのポイント: cdは成功するのに、mkdirやnpm installの実行直後(依存パッケージのダウンロードが始まる前)にEPERMが出て、エラーpathがworktree配下そのものである場合は、これを疑う。
原因3: npmキャッシュの権限崩れによるEPERM
許可リスト配下に作ったworktreeでも、npm installが途中でEPERMで止まることがある。エラーpathをよく見ると~/.npm/_cacache/tmp/のような、worktreeとは無関係の場所を指している。
これはnpmのグローバルキャッシュ(~/.npm)に、過去に別のプロセスや別のユーザー権限で書き込まれたファイルが混在していることが原因だ。npmはインストールのたびにキャッシュへの一時ファイル書き込み→リネームという処理を行うが、そのキャッシュディレクトリ自体の所有権が壊れていると、正規の書き込み処理そのものがEPERMで失敗する。
本来の恒久対処はsudo chown -R $(id -u):$(id -g) ~/.npmでキャッシュの所有権を揃え直すことだが、AIエージェントのセッションはsudoを実行できない前提で動いていることが多い。実務的な回避策は、キャッシュの参照先そのものを、確実に書き込み権限のある一時ディレクトリへ逃がすことになる。
npmまわりの再現性の弱さは、このリポジトリの別の実験でも踏んでいる。Dockerコンテナの中でClaude Codeを動かそうとしたとき、最初に考えたのは「イメージbuild時にnpm install -gで入れればいいのでは」という案だったが、これは見送られた。理由は、npm install -g系の手順はpostinstallでネイティブバイナリを取りに行く挙動に依存しがちで、再現性が落ちるからだ。グローバルキャッシュに依存する設計は、環境が変わった瞬間に壊れやすい——これは_cacacheの権限崩れと根っこが同じ話で、npmが「前回のキャッシュ状態」を暗黙の前提にしている限り、環境をまたぐたびに再現性の綱渡りが続く。
切り分けのポイント: mkdirは成功する(worktree配下にディレクトリは作れる)のに、依存パッケージのダウンロードが始まったあとでEPERMが出て、エラーpathが~/.npm/配下である場合は、これを疑う。
対処:3つの回避策
原因が特定できれば、対処はそれぞれ1つに絞れる。
対処1: 解決済みの絶対パスを固定値として使う
git worktree addの出力、または実行後のgit worktree listで、実際に解決された絶対パスを確認する。以後はそのパスを文字列として固定し、$TMPDIRを都度展開して使い回さない。
# worktreeを作る
git worktree add "$TMPDIR/my-worktree" -b feature/example
# 実際に解決されたパスを確認する($TMPDIRの再展開に頼らない)
git worktree list
# 出力例: /var/folders/xx/xxxxxxxx/T/my-worktree abc1234 [feature/example]
# 以後はこの絶対パスを固定値として使う
cd /var/folders/xx/xxxxxxxx/T/my-worktree
対処2: 許可リスト配下にworktreeを作る
システムの$TMPDIR配下が許可リストと完全一致しない環境では、そもそも$TMPDIR配下にworktreeを作らず、プロジェクトのsandbox設定で明示的に書き込みが許可されているディレクトリ配下に作る。
# 許可リスト配下(例: リポジトリ管理ディレクトリの外側)にworktreeを作る
git worktree add ~/ghq/github.com/<org>/<repo>-worktrees/my-worktree -b feature/example
cd ~/ghq/github.com/<org>/<repo>-worktrees/my-worktree
npm install
プロジェクトごとにsandboxのファイルシステム設定(allowWriteに相当する項目)がどこで定義されているかを確認し、そのパターンに一致するディレクトリを作業場所に選ぶのが確実だ。
対処3: npmキャッシュを一時ディレクトリへ逃がす
~/.npmキャッシュの所有権を直接直せない場合は、キャッシュの参照先自体をそのセッションで確実に書き込める場所に切り替える。
# キャッシュ先を一時ディレクトリへ退避してinstall
npm install --cache "$TMPDIR/npm-cache"
毎回オプションを付けるのが面倒なら、そのセッション内で環境変数として固定してしまってもいい。
export npm_config_cache="$TMPDIR/npm-cache"
npm install
sudoが使えない前提で動いているエージェントからすると、所有権が崩れた本人を放置したまま毎回避けて通るというのは、正直あまり気持ちの良い話ではないんですけど、実務上はこれが一番手数が少ない。ただしこれはあくまで実務的な回避策であり、~/.npm本体の所有権崩れそのものを直すものではない。sudoが使える環境に戻ったタイミングでsudo chown -R $(id -u):$(id -g) ~/.npmを一度実行しておくと、次回以降は回避策なしで済むようになる。
動作確認:自分の環境で再現・検証する
信じるより、自分の手元で確認したほうが早い。次のコマンドで、自分の環境がどの原因を踏みやすいかを確認できる。
# 1. TMPDIRが実際に何を指しているか確認する(呼び出しごとに変わらないか、複数回叩いて比べる)
echo "$TMPDIR"
echo "$TMPDIR"
# 2. worktreeを作り、実際に解決された絶対パスをlistで確認する
git worktree add "$TMPDIR/verify-worktree" -b verify/tmp-check
git worktree list | grep verify-worktree
# 3. そのパスへの書き込み権限があるか、mkdirで直接確認する(npm installを待たずに切り分けられる)
mkdir -p "$TMPDIR/verify-worktree/node_modules" && echo "書き込みOK" || echo "EPERM: 許可リスト外の可能性"
# 4. npmキャッシュディレクトリの所有者を確認する(自分以外のユーザーが混じっていないか)
ls -la ~/.npm/_cacache 2>/dev/null | head -5
find ~/.npm -not -user "$(whoami)" 2>/dev/null | head -5
# 5. キャッシュを一時ディレクトリへ逃がしてinstallが通るか確認する
npm install --cache "$TMPDIR/npm-cache" --dry-run
# 後片付け
git worktree remove "$TMPDIR/verify-worktree" --force 2>/dev/null
git worktree prune
手順4でfindの出力に何か行が出てきたら、それが原因3(npmキャッシュの権限崩れ)の直接的な証拠になる。何も出なければ、原因3は除外して1・2を疑うところから始めればいい。
注意点・Tips
- エラーの「出た段階」を先に固定する:
cdで落ちたか、mkdirで落ちたか、npm installの途中で落ちたか。ここを曖昧にしたまま対処を試すと、無関係な対策を順番に試す羽目になる(3つ全部試して初めて当たりが分かる、というのが一番きつい) - sandbox環境ほど「前回は動いた」を信用しない: 同じコマンドでも、fresh worktreeという「初めての環境」に対して実行している以上、過去の成功実績は保証にならない。以前、git worktreeを増やしすぎてBashのサンドボックスごと止まった話を書いたが、あれも「直前まで動いていたことが何の保証にもならない」という同じ教訓を別の角度で踏んだ例だった
- node_modules自体の増殖はまた別の問題: worktreeごとにnode_modulesが複製されてディスクを圧迫する話は、この記事のインストール失敗とは別の課題としてnode_modulesのディスク肥大化の記事で扱っている。今回の3つのエラーを解消したあと、次にぶつかりやすいのはたいていこちらだ
まとめ
sandbox化されたAIコーディングエージェント環境でfreshなworktreeにnpm installすると、見た目は同じ「installが通らない」でも、原因は$TMPDIR解決の非決定性・ファイルシステムの許可リスト・npmキャッシュの権限崩れという3つの別レイヤーに分かれる。エラーが出た段階(cdかmkdirかnpm installの途中か)とエラーpathの2点を見れば、どの原因かはほぼ機械的に切り分けられる。
対処もそれぞれ独立している。パスは解決済みの絶対値を固定して使い、worktreeは許可リスト配下に作り、npmキャッシュは書き込める場所へ退避する。どれか1つを試して直らなかったからといって「sandboxが壊れている」と結論づける前に、この記事の切り分け表に立ち返ってほしい。同じエラーの顔をしていても、中身はだいたい別人だ。AIエージェントに複数タスクを同時に振って開発を進める比重が増えている場合、こうした環境まわりの型を作るところから相談したいならAI実装支援も選択肢の一つになる。



