CI が落ちた。エラーメッセージは「バイナリが見つからない」。ローカルでは動く。package.json は誰も触っていないし、依存のバージョンも固定されている。ログを遡ると、変わったのは npm 自体のバージョンだけ。
npm 12 で、**第三者パッケージの install スクリプト(postinstall などのライフサイクルスクリプト)が既定で実行されなくなりました。**16年間続いた「入れたら動く」という前提が反転しています。
暗黙の信頼から、明示の許可へ
これまでの npm install は、依存ツリーに含まれるパッケージのライフサイクルスクリプトを、何も言わずに実行していました。数百のパッケージが入る現代の依存ツリーで、これは数百の第三者が自分のマシンとCIで任意のコードを実行できるという意味です。実際、ここ数年のnpmサプライチェーン攻撃の多くは、この経路を使っています。
npm 12 では既定値が反転しました。npm install はまずプロジェクトの package.json にある明示的な許可リストを見て、そこに載っているパッケージのスクリプトだけを実行します。載っていないものは実行されません。
同時に、非レジストリ由来の依存も既定で塞がれました。
| 対象 | 従来 | npm 12 の既定 |
|---|---|---|
| ライフサイクルスクリプト | すべて実行 | 許可リストにあるものだけ実行 |
Git 由来の依存(--allow-git) | 許可 | none(拒否) |
HTTPS の tarball 直指定(--allow-remote) | 許可 | none(拒否) |
Git 依存が塞がれた理由は具体的です。Git 依存のリポジトリが持ち込む .npmrc によって、--ignore-scripts を付けていても Git の実行ファイル自体を差し替えられる経路が存在していました。スクリプトを止めるだけでは閉じない穴だったため、依存の取得元ごと既定で拒否する形になっています。
一番の落とし穴は、エラーが出ないこと
移行で本当に困るのは、スクリプトが止まること自体ではありません。止まったことが通知されないことです。許可リストに無いパッケージのスクリプトは、警告で処理が中断されるのではなく、黙ってスキップされます。
npm install は正常終了します。node_modules も作られます。問題が表面化するのは、その後の工程です。
- ネイティブバイナリを取得・ビルドするパッケージ — 画像処理、ブラウザ自動化、ビルドツール、ORM のクライアント生成など。インストールは成功したのに、実行時に「バイナリが無い」で落ちます
- Git フックを仕込むツール — フックが設定されないので、コミット時のリントやテストが静かに掛からなくなります。壊れるのではなく、守りが外れる
- 設定ファイルや型定義を生成するパッケージ — 生成物が無い状態でビルドに進み、原因から遠い場所で型エラーになります
いずれも症状が原因から離れた場所に出るため、npm のバージョンに辿り着くまでに時間がかかります。CI とローカルで npm のバージョンが違う組織では、「CIだけ落ちる」「特定の人のマシンだけ動く」という形で現れます。

上げる前に、許可リストを先に作る
段取りとしては、npm 12 に上げてから壊れた箇所を潰すのではなく、上げる前に許可リストを作り終えておくのが正解です。npm 11 系には、この移行のための準備コマンドが用意されています。
- 現行の npm 11 系(11.16 以降)で、依存ツリーの中でライフサイクルスクリプトを持つパッケージを列挙する
- 列挙されたものを1件ずつ見て、なぜスクリプトが必要なのかを確認する。ネイティブビルドや必須の生成処理なら許可、理由が説明できないものは許可しない
- 許可すると決めたものだけを
package.jsonの許可リストに書き、コミットする - その状態で npm 12 に上げ、クリーンインストールから通しでビルドとテストを回す
- Git 由来・tarball 直指定の依存が残っていれば、レジストリ経由(社内レジストリを含む)に寄せるか、明示的に許可する
2の工程を飛ばして全部を許可リストに入れると、この変更の意味がまるごと失われます。時間はかかりますが、依存ツリーの中で誰が任意コードを実行しているのかを一度は棚卸しするというのが、この機能の本体です。
CI 側では、npm のバージョンをジョブ定義で固定してください。ランナーの既定バージョンが上がったタイミングで同じ問題が再発します。CI とビルドの資格情報まわりの点検項目はnpmとGitHub Actionsの資格情報を点検する、公開側のフローについてはnpmの段階的公開と人間の承認で扱っています。
発注する側にとっては、許可リストが読める資料になる
ここは受託でシステムを作らせている側にとって、実は使いどころのある変更です。
これまで「このシステムはどの第三者コードを信頼しているか」を確認する手段は、実質ありませんでした。package-lock.json に数千行のパッケージが並んでいても、そのうちどれが自分たちの環境で任意のコードを実行しているのかは分かりません。
npm 12 以降、許可リストは「このプロジェクトがインストール時のコード実行を許した第三者の一覧」そのものになります。行数は多くても数十行です。レビューできる長さで、増えたときは差分で分かります。
そこで、納品物と引き継ぎ資料に次の2つを含めてもらうよう依頼してください。
- 許可リストに載っている各パッケージについて、なぜ許可が必要なのかの1行説明
- 許可リストを増やすときの承認プロセス(誰が判断するか)
依存の追加が自由な状態だと、リストは運用のなかで無言で伸びます。過去のnpmワーム型の攻撃がどう広がったかはnpmワームによる連鎖的な汚染とインシデント対応で整理しました。伸びた分だけ、その攻撃面が広がると考えて差し支えありません。
次にやること
手元のプロジェクトで、依存のうちライフサイクルスクリプトを持つものがいくつあるかを数えてみてください。多くのプロジェクトで、想像より一桁多い数字が出ます。その数がそのまま、これまで無条件に実行を許してきた第三者の数です。
そのうえで、npm のバージョンを CI で固定できているかを確認する。固定していなければ、移行の準備が終わる前に勝手に上がります。
依存の棚卸しや、既存システムの npm 12 移行の判断でお困りの場合は、グリームハブの開発・AI・自動化のご相談で承っています。依存ツリーの規模と構成によって作業量が変わるため、個別にお見積りします。お問い合わせからご相談ください。
Sources
- npm 12 Released: Install Scripts Off by Default as Registry Moves to Explicit Trust — InfoQ
- npm v12 Shifts Security from Implicit to Explicit Trust — JFrog
- Preparing for npm v12: install scripts and non-registry sources become opt-in — GitHub Community Discussion #198547
- npm 12 Disables Install Scripts by Default to Reduce Supply Chain Risk — The Hacker News