npm

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars0
mentions17
01234Oct 4Oct 18Nov 2
UTC days · repositories npm owns0 repositories

Package downloads

No package download history available yet.

Top repositories

npm’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside npm’s account where npm opened or reviewed pull requests in the past 365 days.

Who noticed npm

Everyone who starred or forked npm’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

16posts
Nov 30 posts
  1. src/**/*.js が拾うファイル、ツールごとに違う問題 はじめに src//.ts、!/.test.js、*.{js,ts}。設定ファイルを書いていると、こうした「glob」パターンを毎日のように目にします。 ただ、同じ src/**/*.js でも、ESLint では拾われるファイルが TypeScript の include では拾われない、ということが実際に起きます。この記事では、 glob とは何か(どこまでが標準で、どこからが拡張か) Node.js でよく使われる glob(node-glob)・minimatch・brace-expansion がそれぞれ何をしているか .gitignore / ESLint / tsconfig / Vite / Jest で挙動がどう違うか を、手元で動かせるコードと実行結果つきでまとめます。 動作確認した環境 項目 バージョン Node.js v20.19.2 npm 11.21.1 glob 14.0.3 minimatch 11.0.3(makeRe の確認のみ 10.2.6) brace-expansion 5.0.12 ESLint 10.14.0 TypeScript 7.0.2 Vite 8.3.5 Jest 30.5.2 git 2.47.3 globとは glob は、* や ? を使ってファイルのパス名をまとめて指定するパターン記法です。Linux の man ページ glob(7) によると、起源は UNIX V6 の /etc/glob というプログラムで、まもなくシェルの組み込み機能になりました。 ls *.ts POSIX1 で定められている基本の記号は次の3つです。 記号 意味 * 任意の文字列(空文字列を含む)。パス区切りの / にはマッチしない ? 任意の1文字(/ を除く) [...] 括弧内のいずれか1文字。[!...] で否定、[a-z] で範囲 よく使う ** と {a,b} は、実はこの基本セットに含まれていません。 **(globstar): 0個以上のディレクトリにまたがってマッチする拡張です。bash でも shopt -s globstar を有効にしたときだけ使えます。 {a,b}(ブレース展開): bash のマニュアルでは、ファイル名展開とは別の「文字列を生成する仕組み」として説明されています。 先頭の !(否定パターン): POSIX の記法に出てくる ! は [!abc] のように括弧の中だけです。先頭につけるパターンは定められた記法ではありません。 つまり「glob」と一口に言っても、ツールごとに方言が生まれる余地があります。 Node.js においては、.gitignore に書く node_modules/ や .log、ESLint の設定の files: ["/.ts"]、tsconfig.json の "include": ["src//*"]、テストランナーが *.test.ts を探すときの指定も、すべて glob パターンです。どれも「このパターンに合うファイルはどれか」を判定していますが、それぞれのツールが採用する判定処理には差異があるのです。 この記事ではまず、そもそもglobパターンの判定処理のイメージを掴むために、手で確かめやすい glob・minimatch・brace-expansion というglobパターンを扱うライブラリを代表例として、取り上げます。 代表的な3つのライブラリで仕組みを見る Node.js でよく使われる3つのパッケージ。次のような依存関係でもあります(npm view で確認)。 glob@14.0.3 └─ minimatch ^11.0.2 └─ brace-expansion ^5.0.12 パッケージ やること ディスクを見るか glob パターンに合うファイルをディレクトリをたどって探す 見る minimatch 1つのパス文字列がパターンに合うかを判定する 見ない brace-expansion {a,b} や {1..3} を文字列の配列に展開する 見ない 処理の流れはおおむね次のとおりです。 brace-expansion が src/.{js,ts} を src/.js と src/*.ts に展開する minimatch がそれぞれを / で区切り、区切りごとの照合器を作る glob がディレクトリをたどりながら、各パスを minimatch で判定する 以下のコードは、空のディレクトリで順に実行すれば再現できます。 サンプルコードはGitHubで公開しています: ikushimayujiro/glob-demo 準備:デモ用のファイル構成 mkdir glob-demo && cd glob-demo npm init -y npm i glob@14.0.3 minimatch@11.0.3 minimatch10@npm:minimatch@10.2.6 brace-expansion@5.0.12 minimatch10 は、後で makeRe を見るために v10 系を別名で入れています。 ! minimatch 10.2.6 は、2026年10月31日に公開された GHSA-g3hv-75wg-m249(minimatch <11.0.1 が対象)に該当するため、インストール時に npm audit の警告が出ます。makeRe を確かめるためにわざと古い版を入れているので、このデモ以外では使わないでください。 00-setup.sh #!/usr/bin/env bash set -eu rm -rf fixture && mkdir -p fixture/src/{deep/nested,.config,tests} for f in src/a.js src/b.ts src/.hidden.js src/.config/c.js src/deep/nested/d.js \ src/app.test.js src/tests/e.js src/.config/f.test.js; do echo "export const x = 1;" > "fixture/$f" done fixture/src/ ├── .config/ │ ├── c.js │ └── f.test.js ├── .hidden.js ├── tests/ │ └── e.js ├── a.js ├── app.test.js ├── b.ts └── deep/nested/d.js ドットで始まるファイル(.hidden.js)とディレクトリ(.config/)をわざと混ぜています。 glob:実際にファイルを探す 01-glob.mjs import { glob } from 'glob' const cwd = new URL('./fixture/', import.meta.url).pathname const show = async (pattern, opts = {}) => { const files = (await glob(pattern, { cwd, posix: true, ...opts })).sort() console.log(${pattern} ${JSON.stringify(opts)}\n ->, files) } await show('src/*.js') await show('src//*.js') await show('src//*.js', { dot: true }) await show('src//*.{js,ts}') await show('src//.js', { ignore: ['/.test.js', 'src/__tests__/'] }) src/*.js {} -> [ 'src/a.js', 'src/app.test.js' ] src//*.js {} -> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ] src//*.js {"dot":true} -> [ 'src/.config/c.js', 'src/.config/f.test.js', 'src/.hidden.js', 'src/tests/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ] src//*.{js,ts} -> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/b.ts', 'src/deep/nested/d.js' ] src//.js {"ignore":["/.test.js","src/__tests__/"]} -> [ 'src/a.js', 'src/deep/nested/d.js' ] (出力は読みやすいよう改行を詰めています) ポイントは2つあります。 - は / をまたがないので、src/*.js は deep/nested/d.js を拾いません。 既定では、ドットで始まるファイルやディレクトリはマッチしません。dot: true を付けると拾います。README にも「パターン側で明示的に . を書けば、ドットファイルにマッチする」とあります。 minimatch:1つのパスを判定する minimatch はファイルシステムを見ません。文字列とパターンを受け取って true / false を返すだけです。 02-minimatch.mjs import { minimatch } from 'minimatch' const cases = [ ['src/a.js', 'src/*.js'], ['src/deep/nested/d.js', 'src/*.js'], ['src/deep/nested/d.js', 'src//*.js'], ['src/.hidden.js', 'src/*.js'], ['src/.hidden.js', 'src/*.js', { dot: true }], ['src/.config/c.js', 'src//*.js'], ['src/b.ts', 'src/*.{js,ts}'], ['src/b.ts', 'src/*.{js,ts}', { nobrace: true }], ['src/a.js', '!src/*.ts'], ['src/a.js', 'src/?.js'], ['src/a.js', 'src/[ab].js'], ] for (const [path, pattern, opts] of cases) { console.log(String(minimatch(path, pattern, opts)).padEnd(5), path.padEnd(22), pattern, opts ? JSON.stringify(opts) : '') } console.log('\nbraceExpand("src/.{js,ts}") ->', minimatch.braceExpand('src/.{js,ts}')) true src/a.js src/*.js false src/deep/nested/d.js src/*.js true src/deep/nested/d.js src//*.js false src/.hidden.js src/*.js true src/.hidden.js src/*.js {"dot":true} false src/.config/c.js src//*.js true src/b.ts src/*.{js,ts} false src/b.ts src/*.{js,ts} {"nobrace":true} true src/a.js !src/*.ts true src/a.js src/?.js true src/a.js src/[ab].js braceExpand("src/.{js,ts}") -> [ 'src/.js', 'src/*.ts' ] ** も既定ではドットで始まるディレクトリ(.config/)には入りません。 nobrace: true にするとブレース展開が行われず、{js,ts} は文字どおりの文字列として扱われます。 minimatch 単体では、先頭の ! は「このパターンに合わなければ true」という否定になります。src/a.js は src/*.ts に合わないので true です。 minimatch.braceExpand() の結果からわかるように、ブレースはマッチングの前に展開されて、複数のパターンになります。 makeRe:正規表現になる(v10まで) minimatch v10 系までは、パターンを正規表現に変換して照合していました。makeRe() で中身を見られます。 03-makere.cjs const mm10 = require('minimatch10') // minimatch@10.2.6 const mm11 = require('minimatch') // minimatch@11.0.3 for (const p of ['.js', 'src/*/.js', '.{js,ts}']) { console.log(p.padEnd(14), '->', mm10.makeRe(p)) } console.log('\nminimatch@11 に makeRe はある? ->', typeof mm11.makeRe, '/', typeof mm11.minimatch.makeRe) .js -> /^(?!.)[^/]?.js$/ src/**/.js -> /^src(?:\/|\/(?:(?!(?:\/|^).).)?\/)(?!.)[^/]*?.js$/ .{js,ts} -> /^(?:(?!.)[^/]?.js|(?!.)[^/]*?.ts)$/ minimatch@11 に makeRe はある? -> undefined / undefined 正規表現を読むと、ここまでの挙動がそのまま書かれています。 [^/]?: は / 以外の文字列 (?!.):先頭がドットならマッチしない *.{js,ts}:ブレース展開した2つのパターンを | でつないだもの 一方、2026年10月に出た minimatch v11 では、照合に正規表現を使わなくなりました。changelog によると、パスの区切りごとに @isaacs/nonrecursive-glob-match の Matcher で照合する方式に変わり、makeRe() は削除されています。README では、正規表現はバックトラックによる ReDoS(正規表現によるサービス拒否)を起こしうるため、という理由が説明されています。 ESLint 10.14.0 は minimatch ^10.2.6 に依存しているので、プロジェクトによっては v10 系と v11 系が両方入っていることもあります。 brace-expansion:文字列を展開する 04-brace.mjs import { expand } from 'brace-expansion' console.log(expand('file.{js,ts}')) console.log(expand('img{1..3}.png')) console.log(expand('{a,b}{x,y}')) console.log(expand('src/{components,hooks}/**/*.{ts,tsx}')) [ 'file.js', 'file.ts' ] [ 'img1.png', 'img2.png', 'img3.png' ] [ 'ax', 'ay', 'bx', 'by' ] [ 'src/components//*.ts', 'src/components//*.tsx', 'src/hooks//*.ts', 'src/hooks//*.tsx' ] {a,b}{x,y} が4件になるように、ブレースを並べると展開数は掛け算で増えます。小さい規模で確かめてみます。 for (const n of [1, 2, 4, 8, 12, 16]) { const r = expand('{a,b}'.repeat(n)) const totalChars = r.reduce((s, x) => s + x.length, 0) console.log(n=${String(n).padStart(2)} 入力${String(n * 5).padStart(3)}文字 -> ${String(r.length).padStart(6)}件 (理論値 2^n=${2 ** n}) 合計${totalChars}文字) } n= 1 入力 5文字 -> 2件 (理論値 2^n=2) 合計2文字 n= 2 入力 10文字 -> 4件 (理論値 2^n=4) 合計8文字 n= 4 入力 20文字 -> 16件 (理論値 2^n=16) 合計64文字 n= 8 入力 40文字 -> 256件 (理論値 2^n=256) 合計2048文字 n=12 入力 60文字 -> 4096件 (理論値 2^n=4096) 合計49152文字 n=16 入力 80文字 -> 65536件 (理論値 2^n=65536) 合計1048576文字 入力80文字で、出力は65,536件・合計約100万文字になりました。件数は 2^n、合計文字数は n × 2^n で増えます。 ! ここでは n=16 までにとどめています。これより大きい値を試すとメモリを大量に使うので、手元でも同じくらいの規模にとどめてください。 brace-expansion には、この増え方を抑える上限オプションがあります(README)。 max:展開する件数の上限(既定 100,000) maxLength:展開結果の合計文字数の上限(既定 4,000,000)。5.0.8 で追加 console.log(expand('{1..100}', { max: 10 }).length) // 10 const capped = expand('{a,b}'.repeat(12), { maxLength: 1000 }) console.log(capped.length, capped.reduce((s, x) => s + x.length, 0)) // 83 996 上限に達すると、エラーにはならず結果が途中で打ち切られます。 いざ、ツールごとの違い それではいよいよ、本題。 同じ fixture/ に対して、各ツールが実際にどのファイルを拾うかを試しました(デモの 05-tools.sh)。 ツール 設定項目 使用ライブラリ ドットファイル ** ブレース {a,b} 先頭 ! git .gitignore 独自(fnmatch(3) 準拠の記述) - でマッチする 対応 非対応(文字どおり扱う) 前のパターンで除外したものを再び含める。ただし親ディレクトリが除外されていると戻せない ESLint(flat config) files / ignores minimatch マッチする(ignores はドキュメントに明記、files は実測) 対応 対応 ignores 内で除外の取り消しに使える TypeScript include / exclude 独自(* ? **/ のみ記載) ワイルドカードではマッチしない。src/.config/* のように明示すれば拾う **/ 対応 非対応(該当なしでエラー) 記載なし Vite import.meta.glob() tinyglobby 既定ではマッチしない。exhaustive:true で指定 対応 対応 除外パターンとして使える Jest testMatch micromatch マッチする 対応 対応 除外。配列の順序が効く(否定は後ろに置く) 実測の出力(抜粋)は次のとおりです。 === git (.gitignore) -- .gitignore: src/*.js src/a.js ignored src/.hidden.js ignored src/deep/nested/d.js not-ignored -- .gitignore: src/*.{js,ts} src/a.js not-ignored src/b.ts not-ignored === TypeScript (tsconfig include) Version 7.0.2 -- include: ["src//*"] src/__tests__/e.js src/a.js src/app.test.js src/b.ts src/deep/nested/d.js -- include: ["src//*.{js,ts}"] error TS18003: No inputs were found in config file 'tsconfig.json'. ... -- include: ["src/.config/*"] src/.config/c.js src/.config/f.test.js === ESLint (flat config files: ['src/**/*.{js,ts}']) v10.14.0 src/.config/c.js src/.config/f.test.js src/.hidden.js src/tests/e.js src/a.js src/app.test.js src/b.ts src/deep/nested/d.js === Jest (testMatch) 30.5.2 -- default testMatch src/.config/f.test.js src/tests/e.js src/app.test.js === Vite (import.meta.glob) vite/8.3.5 plain ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/deep/nested/d.js brace ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/b.ts, ./src/deep/nested/d.js negated ./src/tests/e.js, ./src/a.js, ./src/deep/nested/d.js exhaustive ./src/.config/c.js, ./src/.config/f.test.js, ./src/.hidden.js, ... 読み取れることをまとめます。 ドットファイル: ESLint と Jest は .config/ の中まで拾いますが、TypeScript と Vite は拾いません。.storybook/ のような設定ディレクトリが lint やテストの対象に入る・入らないの差は、ここから来ます。 ブレース展開: .gitignore と tsconfig の include では使えません。tsconfig で "src/**/*.{ts,tsx}" と書くと、ファイルが1つも見つからずエラーになりました。 否定: .gitignore の ! は「再び含める」、Jest の testMatch では「除外」で、しかも順序が効きます。同じ記号でも意味が違います。 まとめ:なぜツールごとに方言が生まれるのか ここまで見てきた違いは、大きく3つの原因に分けられます。 1. パターン判別のライブラリが違う git と TypeScript は独自の実装、ESLint は minimatch、Vite は tinyglobby、Jest は micromatch を使っています。 2. 同じライブラリでも、渡すオプションが違う ESLint は minimatch を使っていますが、ドットファイルにもマッチします。minimatch は既定ではドットファイルを拾いませんが、ESLint が dot: true を付けて呼び出しているからです。 3. 設定ファイル側の文法が違う 先頭の ! の意味は、ライブラリだけでなく、各ツールの設定ファイルの文法にもよります。.gitignore では「前のパターンで除外したものを再び含める」、Jest の testMatch では「除外」で、しかも配列の順序が効きます。 同じ glob パターンでも、どのライブラリを、どのオプションで、どんな文法の中で使っているかによって結果が変わるということですね。迷ったら、各ツールのファイル一覧を出すコマンド(git check-ignore、tsc --listFilesOnly、eslint --inspect-config、jest --listTests)で実際の結果を確認するのが確実です。 おまけ:glob と minimatch を書いた人 最後に少し寄り道です。この記事で何度も出てきた glob と minimatch は、npm registry の author 欄を見ると同じ人の名前になっています。 npm view glob author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me) npm view minimatch author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me) Isaac Z. Schlueter さん(GitHub では isaacs)は、npm を作った人です。本人の GitHub プロフィールには「npm inventor, founder npm, Inc. Former Node BDFL.」と書かれています。 author が Isaac Z. Schlueter さんになっているパッケージには、ほかにも次のようなものがあります。 パッケージ 用途 週間ダウンロード数(2026-10-23〜10-29) minimatch glob パターンの照合 951,240,180 lru-cache LRU キャッシュ 789,165,914 glob ファイル検索 536,547,167 rimraf rm -rf 相当の削除 214,603,009 tar tar アーカイブの読み書き 119,327,291 (npm の downloads API で取得) package.json の ^5.0.8 のようなバージョン範囲を解釈する semver も、LICENSE に「Copyright (c) Isaac Z. Schlueter and Contributors」とあります(現在の author 欄は GitHub Inc.、リポジトリは npm organization の管理です)。 普段使っている npm を作った人が、node_modules によく入っている小さな部品もたくさん書いている、ということになります。npm CLI 自身が、glob・minimatch・semver・tar に依存しているのを見ると、開発の中で自身が必要になったものを、片っ端から生み出していった結果なのかもしれません。 参考資料 glob(7) - Linux manual page: man7.org Bash Reference Manual - Brace Expansion: gnu.org Bash Reference Manual - The Shopt Builtin(globstar / dotglob): gnu.org isaacs/node-glob README: isaacs/node-glob isaacs/minimatch README / changelog: isaacs/minimatch , github.com/isaacs/minimatch/blob/main/changelog.md juliangruber/brace-expansion README: juliangruber/brace-expansion gitignore Documentation: git-scm.com ESLint - Configuration Files: eslint.org ESLint v10.14.0 リリースノート: github.com/eslint/eslint/releases/tag/v10.14.0 TSConfig Reference - include: typescriptlang.org Vite - Glob Import: vite.dev tinyglobby documentation: superchupu.dev Jest - testMatch: jestjs.io 脚注 POSIX(Portable Operating System Interface)は、UNIX 系 OS の間でアプリケーションを移植しやすくするための標準規格です。最新版の POSIX.1-2024 は IEEE Std 1003.1-2024 と The Open Group の Base Specifications Issue 8 を兼ねていて、OS のインターフェースに加えてシェルや共通コマンドも定めています。glob の記号は、シェルの章の「2.14 Pattern Matching Notation」で規定されています。出典:The Open Group Base Specifications Issue 8、同 2.14 Pattern Matching Notation、IEEE 1003.1-2024 ↩︎

  2. Debugging Kubernetes: Solving MongoDB CrashLoopBackOff Caused by PVC Version Mismatches When deploying stateful applications on Kubernetes, managing container restarts is usually straightforward. However, things get complicated when your application pod enters a persistent CrashLoopBackOff not because of a code bug, but due to an underlying data compatibility issue in your storage layer. In this article, I will walk you through a real-world scenario where a standard microservice setup (Application + MongoDB) crashed after a deployment change, how I traced the root cause to a database version mismatch inside the Persistent Volume Claim (PVC), and how to safely resolve it without losing data. The Architecture The setup consists of a classic two-tier application deployed inside a local Kubernetes cluster: 1. Application Pod: A standard stateless deployment running a Node.js API backend. 2. Database Pod: A standard Kubernetes Deployment running MongoDB. 3. Storage: A PersistentVolumeClaim (PVC) mounted to the MongoDB deployment at /data/db to ensure database updates persist across pod restarts. The Symptom: Sudden CrashLoopBackOff After applying a deployment update intended to upgrade the infrastructure, the system went down. Running kubectl get pods revealed a frustrating cycle: $ kubectl get pods PS C:\devops-mastered\ai-task-management-system\k8s> kubectl get pods NAME READY STATUS RESTARTS AGE mongo-7884b855cc-2zxtr 1/1 Running 0 3m1s scheduling-test 1/1 Running 3 (2d ago) 27d task-api-8446d7bccc-sgz7w 0/1 CrashLoopBackOff 80 (58s ago) 13d Both the application and the database were failing. The Investigative Trail Step 1: Checking the Application Logs Because the application serves the user requests, I started checking its logs first to see why it was failing its health checks. kubectl logs task-api-8446d7bccc-sgz7w > ai-task-management-system@1.0.0 dev > nodemon --legacy-watch src/server.js npm error path /app npm error command failed npm error signal SIGTERM npm notice npm notice New major version of npm available! 10.8.2 -> 12.2.1 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.1 npm notice To update run: npm install -g npm@12.2.1 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-10-28T06_04_06_402Z-debug-0.log [nodemon] 3.1.14 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,cjs,json [nodemon] starting `node src/server.js` - Log Output: The application logs showed continuous connection timeouts. It was repeatedly attempting to connect to the MongoDB service URI, failing, throwing an unhandled exception, and terminating. This proved the application itself wasn't broken; its dependency (the database) was missing. Step 2: Digging into the MongoDB Container Logs Next, I isolated the core issue by inspecting the database pod's logs. kubectl logs mongo-7884b855cc-2zxtr {"t":{"$date":"2026-10-26T06:38:14.878+00:00"},"s":"F", "c":"CONTROL", "id":20573, "ctx":"initandlisten","msg":"Wrong mongod version","attr":{"error":"UPGRADE PROBLEM: Found an invalid featureCompatibilityVersion document (ERROR: Location4926900: Invalid featureCompatibilityVersion document in admin.system.version: { _id: \"featureCompatibilityVersion\", version: \"8.2\" }. See https://docs.mongodb.com/master/release-notes/8.0-compatibility/#feature-compatibility. :: caused by :: Invalid feature compatibility version value '8.2'. Expected one of the following versions: ''9.0', '8.3', '8.0'. See https://docs.mongodb.com/master/release-notes/8.0-compatibility/#feature-compatibility.). If the current featureCompatibilityVersion is below 8.0, see the documentation on upgrading at https://docs.mongodb.com/master/release-notes/8.0/#upgrade-procedures."}} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"REPL", "id":4784900, "ctx":"initandlisten","msg":"Stepping down the ReplicationCoordinator for shutdown","attr":{"waitTimeMillis":15000}} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"REPL", "id":4794602, "ctx":"initandlisten","msg":"Attempting to enter quiesce mode"} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"-", "id":6371601, "ctx":"initandlisten","msg":"Shutting down the FLE Crud thread pool"} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"COMMAND", "id":4784901, "ctx":"initandlisten","msg":"Shutting down the MirrorMaestro"} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"SHARDING", "id":4784902, "ctx":"initandlisten","msg":"Shutting down the WaitForMajorityService"} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"NETWORK", "id":8314100, "ctx":"initandlisten","msg":"Shutdown: Closing listener sockets"} {"t":{"$date":"2026-10-26T06:38:14.879+00:00"},"s":"I", "c":"NETWORK", "id":4784905, "ctx":"initandlisten","msg":"Shutting down the global connection pool"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"CONTROL", "id":4784906, "ctx":"initandlisten","msg":"Shutting down the FlowControlTicketholder"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"-", "id":20520, "ctx":"initandlisten","msg":"Stopping further Flow Control ticket acquisitions."} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"CONTROL", "id":4784908, "ctx":"initandlisten","msg":"Shutting down PeriodicThreadToAbortExpiredTransactions"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"CONTROL", "id":10036707,"ctx":"initandlisten","msg":"Shutting down PeriodicThreadToRollbackUnderCachePressure"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"REPL", "id":4784909, "ctx":"initandlisten","msg":"Shutting down the ReplicationCoordinator"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"SHARDING", "id":4784910, "ctx":"initandlisten","msg":"Shutting down the ShardingInitializationMongoD"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"REPL", "id":4784911, "ctx":"initandlisten","msg":"Enqueuing the ReplicationStateTransitionLock for shutdown"} {"t":{"$date":"2026-10-26T06:38:14.880+00:00"},"s":"I", "c":"REPL", "id":9945003, "ctx":"initandlisten","msg":"Intent Registry killConflictingOperations","attr":{"interrupt":3}} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"REPL", "id":10286300,"ctx":"thread13","msg":"State transition ops metrics for intent registry","attr":{"metrics":{"lastStateTransition":"SHUTDOWN","totalOpsKilledByIntentRegistry":0}}} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"-", "id":4784912, "ctx":"initandlisten","msg":"Killing all operations for shutdown"} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"-", "id":4695300, "ctx":"initandlisten","msg":"Interrupted all currently running operations","attr":{"opsKilled":4}} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"COMMAND", "id":4784913, "ctx":"initandlisten","msg":"Shutting down all open transactions"} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"REPL", "id":4784914, "ctx":"initandlisten","msg":"Acquiring the ReplicationStateTransitionLock for shutdown"} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"INDEX", "id":4784915, "ctx":"initandlisten","msg":"Shutting down the IndexBuildsCoordinator"} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"EXECUTOR", "id":11280000,"ctx":"initandlisten","msg":"Starting thread pool","attr":{"poolName":"IndexBuildsCoordinatorMongod","numThreads":0,"minThreads":0,"maxThreads":1000000000}} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"NETWORK", "id":4784918, "ctx":"initandlisten","msg":"Shutting down the ReplicaSetMonitor"} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"SHARDING", "id":9439300, "ctx":"initandlisten","msg":"Shutting down the filtering metadata cache"} {"t":{"$date":"2026-10-26T06:38:14.881+00:00"},"s":"I", "c":"-", "id":11437200,"ctx":"initandlisten","msg":"Shutting down the ReadWriteConcernDefaults"} {"t":{"$date":"2026-10-26T06:38:14.882+00:00"},"s":"I", "c":"NETWORK", "id":20562, "ctx":"initandlisten","msg":"Shutdown: Closing open transport sessions"} {"t":{"$date":"2026-10-26T06:38:14.882+00:00"},"s":"I", "c":"NETWORK", "id":4784923, "ctx":"initandlisten","msg":"Shutting down the ASIO transport SessionManager"} {"t":{"$date":"2026-10-26T06:38:14.882+00:00"},"s":"I", "c":"CONTROL", "id":4784928, "ctx":"initandlisten","msg":"Shutting down the TTL monitor"} {"t":{"$date":"2026-10-26T06:38:14.882+00:00"},"s":"I", "c":"CONTROL", "id":4784929, "ctx":"initandlisten","msg":"Acquiring the global lock for shutdown"} {"t":{"$date":"2026-10-26T06:38:14.882+00:00"},"s":"I", "c":"CONTROL", "id":4784930, "ctx":"initandlisten","msg":"Shutting down the storage engine"} {"t":{"$date":"2026-10-26T06:38:14.883+00:00"},"s":"I", "c":"STORAGE", "id":22320, "ctx":"initandlisten","msg":"Shutting down journal flusher thread"} {"t":{"$date":"2026-10-26T06:38:14.883+00:00"},"s":"I", "c":"STORAGE", "id":22321, "ctx":"initandlisten","msg":"Finished shutting down journal flusher thread"} {"t":{"$date":"2026-10-26T06:38:14.884+00:00"},"s":"I", "c":"STORAGE", "id":22322, "ctx":"initandlisten","msg":"Shutting down checkpoint thread"} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"STORAGE", "id":22323, "ctx":"initandlisten","msg":"Finished shutting down checkpoint thread"} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"STORAGE", "id":20282, "ctx":"initandlisten","msg":"Deregistering all the collections"}{"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"STORAGE", "id":10158003,"ctx":"initandlisten","msg":"SpillWiredTigerKVEngine shutting down"} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"STORAGE", "id":10158006,"ctx":"initandlisten","msg":"Closing spill WiredTiger","attr":{"closeConfig":"leak_memory=true,"}} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":886299,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"closing WiredTiger library."}}} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":886500,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"rolling back all running transactions."}}} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":886552,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"closing all running sessions."}}} {"t":{"$date":"2026-10-26T06:38:14.886+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":886647,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"closing some of the internal threads."}}} {"t":{"$date":"2026-10-26T06:38:14.891+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":890995,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"perform final checkpoint and shutting down the global transaction state"}}} {"t":{"$date":"2026-10-26T06:38:14.891+00:00"},"s":"I", "c":"WTCHKPT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":891200,"thread":"1:0x7193159ae600","session_name":"close_ckpt","category":"WT_VERB_CHECKPOINT","log_id":1000000,"category_id":5,"verbose_level":"INFO","verbose_level_id":0,"msg":"Checkpoint requested at stable timestamp (0, 0)"}}} {"t":{"$date":"2026-10-26T06:38:14.892+00:00"},"s":"I", "c":"WTCHKPT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":892800,"thread":"1:0x7193159ae600","session_name":"close_ckpt","category":"WT_VERB_CHECKPOINT_PROGRESS","log_id":1000000,"category_id":7,"verbose_level":"INFO","verbose_level_id":0,"msg":"saving checkpoint snapshot min: 2, snapshot max: 2 snapshot count: 0, oldest timestamp: (0, 0) , meta checkpoint timestamp: (0, 0) base write gen: 1"}}} {"t":{"$date":"2026-10-26T06:38:14.911+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":911129,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"shutdown checkpoint has successfully finished and ran for 19 milliseconds"}}} {"t":{"$date":"2026-10-26T06:38:14.911+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":911653,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1493200,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"shutdown was completed successfully and took 25ms, including 0ms for the rollback to stable, and 19ms for the checkpoint."}}} {"t":{"$date":"2026-10-26T06:38:14.911+00:00"},"s":"I", "c":"WTEVICT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":911779,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_EVICTION","log_id":1000000,"category_id":15,"verbose_level":"INFO","verbose_level_id":0,"msg":"stopping eviction threads"}}} {"t":{"$date":"2026-10-26T06:38:14.911+00:00"},"s":"I", "c":"WTEVICT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":911921,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_EVICTION","log_id":1000000,"category_id":15,"verbose_level":"INFO","verbose_level_id":0,"msg":"waiting for eviction threads to stop"}}} {"t":{"$date":"2026-10-26T06:38:14.912+00:00"},"s":"I", "c":"WTEVICT", "id":22430, "ctx":"thread14","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":912031,"thread":"1:0x71930696b6c0","session_dhandle_name":"file:WiredTigerHS.wt","session_name":"eviction-server","category":"WT_VERB_EVICTION","log_id":1000000,"category_id":15,"verbose_level":"INFO","verbose_level_id":0,"msg":"eviction thread exiting"}}} {"t":{"$date":"2026-10-26T06:38:14.933+00:00"},"s":"I", "c":"STORAGE", "id":10158007,"ctx":"initandlisten","msg":"Closed spill WiredTiger ","attr":{"durationMillis":47}} {"t":{"$date":"2026-10-26T06:38:14.933+00:00"},"s":"I", "c":"STORAGE", "id":22317, "ctx":"initandlisten","msg":"WiredTigerKVEngine shutting down"} {"t":{"$date":"2026-10-26T06:38:14.933+00:00"},"s":"I", "c":"STORAGE", "id":22318, "ctx":"initandlisten","msg":"Shutting down session sweeper thread"} {"t":{"$date":"2026-10-26T06:38:14.935+00:00"},"s":"I", "c":"STORAGE", "id":22319, "ctx":"initandlisten","msg":"Finished shutting down session sweeper thread"} {"t":{"$date":"2026-10-26T06:38:14.936+00:00"},"s":"I", "c":"STORAGE", "id":4795902, "ctx":"initandlisten","msg":"Closing WiredTiger","attr":{"closeConfig":"leak_memory=true,use_timestamp=false,"}} {"t":{"$date":"2026-10-26T06:38:14.937+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":936875,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"closing WiredTiger library."}}} {"t":{"$date":"2026-10-26T06:38:14.937+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":937228,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"rolling back all running transactions."}}} {"t":{"$date":"2026-10-26T06:38:14.937+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":937623,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"closing all running sessions."}}} {"t":{"$date":"2026-10-26T06:38:14.937+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":937722,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"closing some of the internal threads."}}} {"t":{"$date":"2026-10-26T06:38:14.939+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":939834,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"perform final checkpoint and shutting down the global transaction state"}}} {"t":{"$date":"2026-10-26T06:38:14.940+00:00"},"s":"I", "c":"WTCHKPT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":940703,"thread":"1:0x7193159ae600","session_name":"close_ckpt","category":"WT_VERB_CHECKPOINT_PROGRESS","log_id":1000000,"category_id":7,"verbose_level":"INFO","verbose_level_id":0,"msg":"saving checkpoint snapshot min: 2, snapshot max: 2 snapshot count: 0, oldest timestamp: (0, 0) , meta checkpoint timestamp: (0, 0) base write gen: 17595"}}} {"t":{"$date":"2026-10-26T06:38:14.979+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":979453,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1000000,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"shutdown checkpoint has successfully finished and ran for 39 milliseconds"}}} {"t":{"$date":"2026-10-26T06:38:14.980+00:00"},"s":"I", "c":"WTRECOV", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":979960,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_RECOVERY_PROGRESS","log_id":1493200,"category_id":35,"verbose_level":"INFO","verbose_level_id":0,"msg":"shutdown was completed successfully and took 42ms, including 0ms for the rollback to stable, and 39ms for the checkpoint."}}} {"t":{"$date":"2026-10-26T06:38:14.980+00:00"},"s":"I", "c":"WTEVICT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":980140,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_EVICTION","log_id":1000000,"category_id":15,"verbose_level":"INFO","verbose_level_id":0,"msg":"stopping eviction threads"}}} {"t":{"$date":"2026-10-26T06:38:14.980+00:00"},"s":"I", "c":"WTEVICT", "id":22430, "ctx":"initandlisten","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":980220,"thread":"1:0x7193159ae600","session_name":"WT_CONNECTION.close","category":"WT_VERB_EVICTION","log_id":1000000,"category_id":15,"verbose_level":"INFO","verbose_level_id":0,"msg":"waiting for eviction threads to stop"}}} {"t":{"$date":"2026-10-26T06:38:14.980+00:00"},"s":"I", "c":"WTEVICT", "id":22430, "ctx":"thread15","msg":"WiredTiger message","attr":{"message":{"ts_sec":1792996694,"ts_usec":980288,"thread":"1:0x71930f17c6c0","session_dhandle_name":"file:WiredTigerHS.wt","session_name":"eviction-server","category":"WT_VERB_EVICTION","log_id":1000000,"category_id":15,"verbose_level":"INFO","verbose_level_id":0,"msg":"eviction thread exiting"}}} {"t":{"$date":"2026-10-26T06:38:14.999+00:00"},"s":"I", "c":"STORAGE", "id":4795901, "ctx":"initandlisten","msg":"WiredTiger closed","attr":{"durationMillis":63}} {"t":{"$date":"2026-10-26T06:38:15.001+00:00"},"s":"I", "c":"-", "id":4784931, "ctx":"initandlisten","msg":"Dropping the scope cache for shutdown"} {"t":{"$date":"2026-10-26T06:38:15.001+00:00"},"s":"I", "c":"CONTROL", "id":10500904,"ctx":"initandlisten","msg":"Shutting down OpenTelemetry metrics"} {"t":{"$date":"2026-10-26T06:38:15.001+00:00"},"s":"I", "c":"-", "id":10175800,"ctx":"initandlisten","msg":"Shutting down the standalone executor"} {"t":{"$date":"2026-10-26T06:38:15.001+00:00"},"s":"I", "c":"CONTROL", "id":20565, "ctx":"initandlisten","msg":"Now exiting"} {"t":{"$date":"2026-10-26T06:38:15.001+00:00"},"s":"I", "c":"STORAGE", "id":22279, "ctx":"initandlisten","msg":"shutdown: removing fs lock..."} {"t":{"$date":"2026-10-26T06:38:15.001+00:00"},"s":"I", "c":"CONTROL", "id":8423404, "ctx":"initandlisten","msg":"mongod shutdown complete","attr":{"Summary of time elapsed":{"Statistics":{"enterTerminalShutdownMillis":0,"stepDownReplCoordMillis":0,"quiesceModeMillis":0,"stopFLECrudMillis":0,"shutDownMirrorMaestroMillis":0,"shutDownWaitForMajorityServiceMillis":0,"shutDownGlobalConnectionPoolMillis":0,"shutDownSearchTaskExecutorsMillis":0,"shutDownFlowControlTicketHolderMillis":1,"shutDownAbortExpiredTransactionsThreadMillis":0,"shutDownRollbackUnderCachePressureThreadMillis":0,"killAllOperationsMillis":0,"shutDownOpenTransactionsMillis":0,"acquireRSTLMillis":0,"shutDownIndexBuildsCoordinatorMillis":0,"shutDownReplicaSetMonitorMillis":0,"shutDownAndJoinReadWriteConcernDefaultsMillis":0,"shutDownTransportLayerMillis":1,"shutDownTTLMonitorMillis":0,"shutDownStorageEngineMillis":117,"shutDownFTDCMillis":0,"shutDownFlowControlMillis":0,"shutDownOtelMetricsMillis":0,"shutDownOtelTracesMillis":0,"shutDownReplicaSetNodeExecutorMillis":0,"closeLockFileMillis":0,"shutDownOCSPMillis":0,"shutdownTaskTotalMillis":122}}}} {"t":{"$date":"2026-10-26T06:38:15.002+00:00"},"s":"I", "c":"CONTROL", "id":23138, "ctx":"initandlisten","msg":"Shutting down","attr":{"exitCode":62}}``` * **The Smoking Gun:** The database logs revealed that the MongoDB engine was initializing, reading the data files from the mounted PVC. The logs explicitly stated a version incompatibility: the existing data directory in the PVC was formatted for **MongoDB version 8.2**, but the new container binary running was **MongoDB version 9.1**. Because major database engines generally prevent automated, backwards-incompatible storage engine upgrades without an explicit migration path, the MongoDB process automatically terminated to protect the data from corruption. This constant termination triggered the Kubernetes `CrashLoopBackOff`. **The Resolution: Restoring Stability Safely** In a production-like learning environment, the primary rule is **do not lose or corrupt the database data**. Since I needed to move ahead with testing the project without destroying the existing database state, the safest operational move was to downgrade the container image version back to match the data files. I modified the MongoDB deployment manifest, replacing the unpinned or upgraded image tag back to the exact compatible version: Before (Caused the crash) spec: containers: - name: mongo image: mongo:latest After (The Fix) spec: containers: - name: mongo image: mongo:8.2.12 After applying the corrected manifest using kubectl apply -f mongo-deployment.yaml, the database engine successfully read the older data directory without errors. Once the MongoDB pod transitioned to the Running state, the application pod automatically re-connected on its next restart cycle, stabilizing the entire cluster. Key DevOps Takeaways 1. Never use generic or unpinned image tags (like latest) for Stateful Workloads: Always pin your database images to explicit minor versions (mongo:8.2) to prevent accidental automated upgrades when pods recreate. 2. Deploy Databases via StatefulSets for Production: While a standard Deployment works for simple learning projects, production databases should use StatefulSets to properly map specific network identities to stable persistent volumes. 3. Database Upgrades Require a Strategy: You cannot upgrade a database by simply bumping the image tag in Kubernetes. You must follow the database vendor’s official migration path, which usually involves backing up data, spinning up a temporary migration pod, running upgrade scripts, and switching the storage layer over.

  3. npm minimum release age: a cooldown for dependencies Setting an npm minimum release age is, for me, the cheapest security measure you can add to a JavaScript project this week. One line in your .npmrc, and npm only installs package versions that are already a few days old. That sounds trivial, but it targets exactly the moment when supply chain attacks hit. We have seen the pattern plenty of times since 2025: someone takes over a maintainer account, publishes a version with malware, and it stays available for a few hours up to a few days until scanners or the community report it and the registry removes it. Anyone who runs npm install during that window gets it. That might be your laptop, your CI pipeline or an AI agent that “quickly” adds a dependency. A cooldown changes only one thing: you are no longer among the first. On top of that, GitHub put a package firewall with exactly this kind of waiting period into preview on October 20. High time to set this up properly. > In short > > A cooldown only lets new package versions into your project after a waiting period: in npm via min-release-age, in pnpm, Yarn and Bun via their own settings with different units, and in Dependabot and Renovate at the level of update PRs. The key finding from my test: npm ci and an npm install with an existing lockfile do not check the age, so a lockfile check belongs in CI. The biggest pitfall is urgent security fixes, for which you need a deliberate exception path. What you will learn in this article - How npm min-release-age works and how to set it up in five minutes - Which settings pnpm, Yarn and Bun offer and why the units are tricky - Why npm ci bypasses the cooldown and how a lockfile check catches it - How to configure Dependabot and Renovate to match the waiting period - How to ship an urgent security fix right away anyway What does the npm minimum release age do? Before npm installs anything, it builds a dependency tree: for every range such as "zod": "^4.1.0" it looks for the newest matching version. With min-release-age=3 in the configuration, only versions published more than three days ago qualify. If there is a matching older version, npm simply takes that one. If there is none, the command fails with an error. The option was introduced in npm 11.10.0 in February 2026. There is also the older option before. It takes a fixed date instead of a number of days. If both are set in the same configuration source, before wins according to the npm docs. Across sources the usual order applies: command line before environment variable before project .npmrc before user and global configuration. That matters later for the exception path. One more term, because it comes up constantly: the lockfile (for npm, the package-lock.json) records which version of each package was actually installed, including all indirect dependencies. If you commit it, every machine gets the same tree. > 🎬 Cooldown: a waiting period for new package versions – this animated diagram only runs on my site: watch it on sascha-ms.de Three layers for a cooldown You can add the waiting period in three places, and each one protects something different. So it is worth a quick look at the big picture before you touch any configuration files. !Diagram: npm minimum release age in four stages – registry proxy, update bot, package manager and CI lockfile check Four places where package versions that are too new get stopped - Package manager (npm, pnpm, Yarn, Bun): applies whenever versions are resolved again, i.e. with npm install package, npm update or a fresh lockfile. - Update bot (Dependabot, Renovate): applies before a pull request with a new version even exists. - Registry proxy (GitHub package firewall or your own proxy): applies centrally to every machine that installs through it, including CI runners and agents whose configuration you do not control. For most teams the first two layers plus a check in CI are enough. The third one is mainly interesting for larger organizations, more on that below. How to set up npm min-release-age Prerequisites - npm 11.10.0 or newer for min-release-age, npm 12 for the exclusion list min-release-age-exclude. Check with npm -v, update with npm install -g npm@12. - A project with a committed package-lock.json. - For the lockfile check further down: Node.js 22.18 or newer (TypeScript files run there without a build step). Step 1: Create the .npmrc in your project Create an .npmrc in the project folder or extend the existing one. The exclusion for your own scope is optional but sensible: you do not want to block internal packages that your team publishes itself for three days. # .npmrc min-release-age=3 min-release-age-exclude[]=@bookshop/* Commit the file. That way the rule applies to everyone on the team and in CI, not just on your machine. If you also want the protection for all your own projects, put the same line into your user configuration (npm config set min-release-age 3). Step 2: Test whether the waiting period applies The easiest test uses a package that publishes new versions every day. TypeScript ships a nightly build under the next tag. Explicitly request a version from today: npm install typescript@7.1.0-dev.20261028.1 In my test with npm 12.2.1, I got exactly what you want to see: npm error code ETARGET npm error notarget No matching version found for typescript@7.1.0-dev.20261028.1 with a date before 10/25/2026, 9:38:42 AM. If you only ask for typescript@next, on the other hand, npm silently resolves to an older version. In my case the stable 7.0.2 ended up in the project instead of today’s nightly build. That is convenient, but it can surprise you when you deliberately want to test a prerelease. Step 3: Define an exception path for hotfixes Sooner or later a security fix appears that you do not want to leave lying around for three days. Because the command line overrides the project configuration, this is enough for a single, consciously reviewed case: npm install ky@latest --min-release-age=0 Watch out for npm audit fix: if the cooldown blocks the patched release, npm keeps the vulnerable version according to the docs, prints a warning and exits with an error code. A pipeline that runs npm audit fix automatically will fail in that case. That is intended, but your team should know what to do in that moment. pnpm, Yarn and Bun compared The other package managers partly had the idea earlier and name it differently. The units are the real danger: the same number 3 means three days in npm, three minutes in pnpm and three seconds in Bun. | Package manager | Setting | Unit | Exclusions | | --- | --- | --- | --- | | npm | min-release-age in .npmrc | days | min-release-age-exclude (npm 12 and later) | | pnpm | minimumReleaseAge in pnpm-workspace.yaml | minutes (default 1440 per current docs) | minimumReleaseAgeExclude | | Yarn (Berry) | npmMinimalAgeGate in .yarnrc.yml | duration, e.g. 1w | npmPreapprovedPackages | | Bun | install.minimumReleaseAge in bunfig.toml | seconds | minimumReleaseAgeExcludes | # pnpm-workspace.yaml minimumReleaseAge: 4320 # 3 days in minutes minimumReleaseAgeExclude: - '@bookshop/*' # bunfig.toml [install] minimumReleaseAge = 259200 # 3 days in seconds minimumReleaseAgeExcludes = ["@bookshop/prices"] With pnpm, two neighboring settings are worth a look as well: minimumReleaseAgeStrict is enabled automatically as soon as you set minimumReleaseAge yourself, and it makes the installation fail when no version is old enough. minimumReleaseAgeIgnoreMissingTime, however, defaults to true. If a private registry does not return a publish date, the package is waved through without a check. The gap: npm ci does not check the age Now comes the part that kept me busy the most while trying this out. I created a lockfile without a cooldown that contained today’s TypeScript nightly. After that I set min-release-age=3 and reinstalled: rm -rf node_modules npm ci npm install Both commands ran without errors, and the folder still contained the version from that same morning. This is not a bug. The lockfile is a decision that has already been made, and the cooldown only applies when npm resolves versions again. As a result, a lockfile with very new versions can reach your repository in several ways: - Someone still works with npm 10 or an early npm 11 version. Those do not know the option and ignore it. In my test, npm 10.9 installed the nightly without any comment. - A bot or AI agent updates the lockfile with its own configuration. - Someone bypasses the rule locally with --min-release-age=0 and forgets to mention it in the review. The consequence: the waiting period belongs not only in the .npmrc but also in CI as a check. Specifically, for the versions a pull request newly adds to the lockfile. A lockfile check in CI with TypeScript The idea is simple: the script compares the pull request’s lockfile with the one from the target branch, collects all new or changed package versions and asks the registry for their publish dates. The npm registry provides them in the time field of the package metadata (the so-called packument). If a version is newer than allowed or the date is missing, the check fails. The script has no runtime dependencies and runs directly as TypeScript on Node.js 22.18 or newer. Step 1: Create the project Create a folder tools/release-age-guard in the repository. You only need TypeScript for type checking; at runtime Node strips the types itself. That is why the tsconfig.json contains erasableSyntaxOnly: it makes the compiler report syntax that Node cannot simply drop, such as enum. { "name": "release-age-guard", "version": "1.0.0", "private": true, "type": "module", "engines": { "node": ">=22.18" }, "scripts": { "check": "node src/cli.ts", "typecheck": "tsc --noEmit", "test": "node --test" }, "devDependencies": { "@types/node": "^22.18.0", "typescript": "^7.0.2" } } { "compilerOptions": { "target": "es2023", "module": "nodenext", "moduleResolution": "nodenext", "strict": true, "noEmit": true, "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "types": ["node"] }, "include": ["src", "test"] } Step 2: Read and compare the lockfile In a package-lock.json (version 2 or 3), the packages section lists every installed path, for example node_modules/a/node_modules/zod. The package name is everything after the last node_modules/. The function skips workspace folders, links and local paths because they do not come from the registry. // src/lockfile.ts export interface LockedPackage { name: string; version: string; } interface LockfileV3 { lockfileVersion?: number; packages?: Record<string, { version?: string; link?: boolean; resolved?: string }>; } // Reads all installed packages from a package-lock.json (lockfileVersion 2 or 3). export function readLockedPackages(json: string): LockedPackage[] { const lock = JSON.parse(json) as LockfileV3; if (!lock.packages) { throw new Error('package-lock.json without "packages" – please use lockfileVersion 2 or 3'); } const result = new Map<string, LockedPackage>(); for (const [path, entry] of Object.entries(lock.packages)) { // Installed packages only; skip workspace folders and links if (!path.includes('node_modules/') || entry.link || !entry.version) continue; // Local paths and git dependencies have no registry tarball if (entry.resolved && !entry.resolved.startsWith('http')) continue; const name = path.slice(path.lastIndexOf('node_modules/') + 'node_modules/'.length); result.set(`${name}@${entry.version}`, { name, version: entry.version }); } return [...result.values()]; } // Returns only packages that are new or have a different version in the new lockfile. export function changedPackages(base: LockedPackage[], head: LockedPackage[]): LockedPackage[] { const known = new Set(base.map((p) => `${p.name}@${p.version}`)); return head.filter((p) => !known.has(`${p.name}@${p.version}`)); } Step 3: Calculate the age and collect violations The actual rule lives in a pure function. It receives the source of the date as a function argument. This lets you test it without a network. Exclusions work like in npm: exact names or a scope with /*. // src/release-age.ts import type { LockedPackage } from './lockfile.ts'; export type PublishTimeLookup = (pkg: LockedPackage) => Promise<Date | undefined>; export interface AgeViolation { pkg: LockedPackage; ageDays: number | undefined; // undefined = the registry has no publish date } export interface AgeCheckOptions { minDays: number; exclude: string[]; // package names or patterns like "@bookshop/*" now?: Date; } const DAY_MS = 24 * 60 * 60 * 1000; export function isExcluded(name: string, patterns: string[]): boolean { return patterns.some((pattern) => pattern.endsWith('/*') ? name.startsWith(pattern.slice(0, -1)) : name === pattern, ); } export async function findTooYoung( packages: LockedPackage[], lookup: PublishTimeLookup, options: AgeCheckOptions, ): Promise<AgeViolation[]> { const now = options.now ?? new Date(); const candidates = packages.filter((pkg) => !isExcluded(pkg.name, options.exclude)); const checks = await Promise.all( candidates.map(async (pkg) => { const published = await lookup(pkg); if (!published) return { pkg, ageDays: undefined }; const ageDays = (now.getTime() - published.getTime()) / DAY_MS; return { pkg, ageDays }; }), ); // A missing date counts as a violation: better to look once too often return checks.filter((c) => c.ageDays === undefined || c.ageDays < options.minDays); } Step 4: Query the registry For each package the script loads the metadata exactly once, even if several versions of it appear in the lockfile. Scoped names such as @bookshop/prices need an encoded / in the URL. If your team uses its own registry, pass it with --registry. // src/registry.ts import type { LockedPackage } from './lockfile.ts'; interface Packument { time?: Record<string, string>; } // Asks the registry for the publish date of exactly this version. export function createRegistryLookup(registry = 'https://registry.npmjs.org') { const cache = new Map<string, Promise<Packument>>(); const base = registry.replace(/\/$/, ''); const load = (name: string): Promise<Packument> => { let pending = cache.get(name); if (!pending) { const url = `${base}/${name.replace('/', '%2F')}`; pending = fetch(url, { headers: { accept: 'application/json' } }).then(async (res) => { if (!res.ok) throw new Error(`Registry answered ${res.status} for ${name}`); return (await res.json()) as Packument; }); cache.set(name, pending); } return pending; }; return async (pkg: LockedPackage): Promise<Date | undefined> => { const doc = await load(pkg.name); const iso = doc.time?.[pkg.version]; return iso ? new Date(iso) : undefined; }; } Step 5: The command line script The script reads the minimum age from the .npmrc, so there is only one source of truth. You can override it with --min-days. // src/cli.ts import { readFileSync, existsSync } from 'node:fs'; import { parseArgs } from 'node:util'; import { readLockedPackages, changedPackages } from './lockfile.ts'; import { findTooYoung } from './release-age.ts'; import { createRegistryLookup } from './registry.ts'; const { values } = parseArgs({ options: { base: { type: 'string' }, // lockfile from the target branch, e.g. from git show head: { type: 'string', default: 'package-lock.json' }, 'min-days': { type: 'string' }, exclude: { type: 'string', multiple: true, default: [] }, registry: { type: 'string', default: 'https://registry.npmjs.org' }, }, }); // Without --min-days, the value from the project .npmrc applies function minDaysFromNpmrc(): number | undefined { if (!existsSync('.npmrc')) return undefined; const match = readFileSync('.npmrc', 'utf8').match(/^\s*min-release-age\s*=\s*(\d+)/m); return match ? Number(match[1]) : undefined; } const minDays = Number(values['min-days'] ?? minDaysFromNpmrc() ?? 3); const head = readLockedPackages(readFileSync(values.head, 'utf8')); const base = values.base ? readLockedPackages(readFileSync(values.base, 'utf8')) : []; const toCheck = changedPackages(base, head); console.log(`Checking ${toCheck.length} new or changed packages (minimum age ${minDays} days)`); const violations = await findTooYoung(toCheck, createRegistryLookup(values.registry), { minDays, exclude: values.exclude, }); for (const { pkg, ageDays } of violations) { const age = ageDays === undefined ? 'without a date' : `${ageDays.toFixed(1)} days old`; console.error(`✗ ${pkg.name}@${pkg.version} is ${age}`); } if (violations.length > 0) { console.error(`${violations.length} package(s) younger than ${minDays} days – please review the lockfile.`); process.exit(1); } console.log('✓ All new versions have passed the waiting period.'); Step 6: Test it The tests use Node’s built-in test runner. Two commands are enough for type checking and testing: // test/release-age.test.ts import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readLockedPackages, changedPackages } from '../src/lockfile.ts'; import { findTooYoung, isExcluded } from '../src/release-age.ts'; const lock = (packages: Record<string, object>) => JSON.stringify({ lockfileVersion: 3, packages: { '': { name: 'bookshop' }, ...packages } }); const reg = (name: string, v: string) => ({ version: v, resolved: `https://registry.npmjs.org/${name}/-/${name.split('/').pop()}-${v}.tgz`, }); test('reads nested and scoped packages from the lockfile', () => { const pkgs = readLockedPackages( lock({ 'node_modules/zod': reg('zod', '4.1.0'), 'node_modules/@bookshop/prices': reg('@bookshop/prices', '2.0.0'), 'node_modules/a/node_modules/zod': reg('zod', '3.25.0'), 'packages/catalog': { version: '1.0.0' }, }), ); assert.deepEqual( pkgs.map((p) => `${p.name}@${p.version}`).sort(), ['@bookshop/prices@2.0.0', 'zod@3.25.0', 'zod@4.1.0'], ); }); test('checks only new or changed versions', () => { const base = [{ name: 'zod', version: '4.1.0' }, { name: 'ky', version: '1.8.0' }]; const head = [{ name: 'zod', version: '4.1.0' }, { name: 'ky', version: '1.9.0' }]; assert.deepEqual(changedPackages(base, head), [{ name: 'ky', version: '1.9.0' }]); }); test('reports versions that are too new or have no date', async () => { const now = new Date('2026-10-28T12:00:00Z'); const published: Record<string, string | undefined> = { 'ky@1.9.0': '2026-10-27T12:00:00Z', // 1 day old 'zod@4.1.0': '2026-10-01T12:00:00Z', // 27 days old 'mystery@0.1.0': undefined, }; const pkgs = Object.keys(published).map((key) => { const [name, version] = key.split('@'); return { name, version }; }); const violations = await findTooYoung( pkgs, async (p) => { const iso = published[`${p.name}@${p.version}`]; return iso ? new Date(iso) : undefined; }, { minDays: 3, exclude: [], now }, ); assert.deepEqual( violations.map((v) => [v.pkg.name, v.ageDays]), [['ky', 1], ['mystery', undefined]], ); }); test('exclusions apply to names and scopes', () => { assert.equal(isExcluded('@bookshop/prices', ['@bookshop/*']), true); assert.equal(isExcluded('@bookshopx/prices', ['@bookshop/*']), false); assert.equal(isExcluded('zod', ['zod']), true); }); npm install npm run typecheck npm test Expected result: four green tests. Against the real registry, the run in my test project with the nightly build looked like this: Checking 8 new or changed packages (minimum age 3 days) ✗ @typescript/typescript-darwin-arm64@7.1.0-dev.20261028.1 is 0.1 days old ✗ @typescript/typescript-linux-x64@7.1.0-dev.20261028.1 is 0.1 days old ✗ typescript@7.1.0-dev.20261028.1 is 0.1 days old … 8 package(s) younger than 3 days – please review the lockfile. The second run with --exclude typescript is interesting: afterwards seven violations remained, namely the platform-specific packages that TypeScript 7 brings along. An exclusion only applies to the named package, not to its dependencies. The npm docs describe min-release-age-exclude the same way. Step 7: Add it to GitHub Actions The workflow only runs when a pull request changes the lockfile. It fetches the version from the target branch and passes it in as the baseline. The check itself does not need npm install. # .github/workflows/release-age.yml name: release-age on: pull_request: paths: - package-lock.json permissions: contents: read jobs: lockfile-age: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: actions/setup-node@v7 with: node-version: 24 - name: Fetch the lockfile from the target branch run: git show "origin/${{ github.base_ref }}:package-lock.json" > "$RUNNER_TEMP/base-lock.json" - name: Check the age of new package versions run: node tools/release-age-guard/src/cli.ts --base "$RUNNER_TEMP/base-lock.json" --exclude "@bookshop/*" Two security notes: the workflow only needs read permissions, and in a real project you should pin the actions to a commit hash instead of a tag. The cost is negligible, a run takes a few seconds. Only very large packages have metadata of several megabytes, which is why the script only queries changed packages. Since late October 2026 there is a more readable way: I show how the GitHub Actions lockfile separates tags from commits in a separate article. Dependabot cooldown and Renovate minimumReleaseAge The update bots are the second layer. They make sure that no pull request with a brand-new version is created in the first place, and they are the reason the lockfile check rarely fires in everyday work. Dependabot According to the current GitHub docs, Dependabot already applies a three-day waiting period to version updates by default. With the cooldown block you can fine-tune it, separately for major, minor and patch updates. Security updates are explicitly excluded and arrive immediately. # .github/dependabot.yml version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" cooldown: default-days: 3 semver-major-days: 14 semver-minor-days: 7 semver-patch-days: 3 exclude: - "@bookshop/*" Renovate Renovate calls the option minimumReleaseAge and expects a duration as text. As long as a version is too new, Renovate does not create a branch at all with the default setting internalChecksFilter: "strict". When generating the npm lockfile, Renovate also passes --before itself, so indirect dependencies are not too fresh either. Here, too, security updates bypass the waiting period. { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "minimumReleaseAge": "3 days", "packageRules": [ { "matchUpdateTypes": ["major"], "minimumReleaseAge": "14 days" }, { "matchPackageNames": ["@bookshop/**"], "minimumReleaseAge": null } ] } Since Renovate 42, a missing timestamp counts as “not old enough yet”. For npm this is not an issue, but with some container registries Renovate holds back digest updates indefinitely as a result. My rule of thumb for both bots: the waiting period in the bot is at least as long as the one in the .npmrc. Otherwise you get pull requests that could not even be reproduced locally with a fresh install. GitHub package firewall for larger organizations On October 20, 2026, GitHub released the package firewall as a public preview. It sits between your machines and the npm registry and checks both metadata requests and downloads against policies that administrators define for the whole enterprise. These include a configurable waiting period, checks against the GitHub Advisory Database for malware and vulnerabilities, and checks such as a downgrade in attestations. If the firewall blocks a package by mistake, developers can request an exception. The limitations are clear: so far it only supports npm and is only available to GitHub Enterprise Cloud customers. It is free during the preview and is supposed to be billed by usage afterwards. I mainly see its appeal for agents and CI runners whose configuration you do not control in every repository. For a single team it does not replace the three configuration files from this article; it complements them. How long should the cooldown be? My recommendation to start with: three days in the package manager, and in the bot three days for patches, seven for minor and 14 for major updates. The three days are not arbitrary. The npm registry allows authors to unpublish a package within 72 hours of publishing it. If you wait three days, you no longer install a version that might still disappear at short notice. Majors get more time because they mean work anyway and an extra week rarely matters. I often hear two objections. The first: “If everyone waits, who will find the malicious version?” In the well-known cases from 2025, it was mostly the automated scanners of security companies that raised the alarm first, since they analyze every new version right away. The early installers were victims rather than discoverers. Still, the objection is not entirely unfounded, so I would not go up to several weeks. The second objection: “Then security fixes arrive too late.” For the bots that is not true, because security updates bypass the waiting period. Locally you need the deliberate exception path from step 3. Where I clearly disagree: a cooldown is no substitute for other measures. It does not help against packages that have been malicious for a long time, nor against install scripts of packages you trust anyway. A committed lockfile, npm ci in the pipeline, install scripts only for approved packages and trusted publishing for your own packages remain mandatory. The cooldown only takes the most dangerous moment off your hands: the first day of a new version. How quickly such a moment can escalate is something I described in the article on the Node.js vulnerability Node2Shell, even though that one was a flaw in Node itself. Common mistakes with an npm cooldown - Mixing up units: minimumReleaseAge: 3 in pnpm means three minutes. Add the unit as a comment. - Old npm version on the team: npm before 11.10 ignores the option without an error. Set the minimum version in engines and check it in CI. - Exclusion too broad: an entry in the exclusion list only applies to the name, not to its dependencies. For hotfixes, prefer a targeted --min-release-age=0 on the single command. - Missing timestamps: private registries or proxies do not always return a time field. pnpm waves such packages through by default; the lockfile check from this article reports them. - Only the .npmrc, no check: without a CI check, new versions slip through existing lockfiles, see above. If you are working on your review process anyway: bot PRs with dependency updates combine well with the ideas from my post on AI-assisted code fixes with AWS Continuum. FAQ What is npm min-release-age? An npm configuration option that defines how many days old a package version must be before npm installs it. Newer versions are skipped during resolution. Which npm version introduced min-release-age? The option arrived with npm 11.10.0 in February 2026. The exclusion list min-release-age-exclude has been available since npm 12. Does min-release-age apply to npm ci? No. npm ci installs exactly what the lockfile says. In my test, npm install with an existing lockfile also ran without an age check. That is why checking new lockfile entries in CI is worth it. Does a cooldown delay security updates? Not with Dependabot and Renovate; both exclude security updates. Locally and in the pipeline you need a deliberate exception path, for example --min-release-age=0 for exactly one command. Your next step Open your most important repository, check the version with npm -v and create the two-line .npmrc from step 1. That takes five minutes. You can add the lockfile check afterwards at your own pace. And now I am curious: how long is the waiting period on your team, and have you ever missed a hotfix because of a cooldown? Feel free to tell me in the comments. Sources - npm Docs: Config (min-release-age, min-release-age-exclude, before) - npm CLI: Release v11.10.0 (“add min-release-age”) - npm CLI: Changelog (min-release-age-exclude in npm 12) - pnpm: Dependency resolution settings (minimumReleaseAge) - Yarn: .yarnrc.yml (npmMinimalAgeGate) - Bun: bunfig.toml (install.minimumReleaseAge) - GitHub Docs: Dependabot options reference (cooldown) - Renovate Docs: Minimum Release Age - GitHub Changelog: GitHub package firewall in public preview (October 20, 2026) - GitHub Changelog: Dependabot now supports npm 12, pnpm 11 and 12, and Bun 1.4 - Tested with Node.js 22.23, npm 12.2.1 and npm 10.9.9 (behavior of npm ci and npm install with an existing lockfile), TypeScript 7.0.2 (tsc --noEmit) and the Node test runner (4 tests). The lockfile check ran against the real npm registry. The GitHub workflow was not executed. This post was created with AI assistance. --- 👉 Next step: Not sure which AWS service fits your use case? My AWS service finder asks a few questions and points you to the right one. Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de

    1 reaction
  4. Qiita · · @yukmaru · npm/cli

    stackblitzでCannot find native binding. npm has a bug related to optional dependencies... エラーが出る はじめに stackblitzにて、Vanila(JavaScript)でプロジェクトを作成したらRunning start commandでエラーが出たのでまとめます 問題 プロジェクト作成したらターミナルで、``npm install && npm run dev`` が実行されます。その際に下記のエラーが発生しました。 Error: Cannot find native binding. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory. at eval (file:///home/projects/vitejs-vite-vlfdrkbx/node_modules/rolldown/dist/shared/binding-BMtuZhvP.mjs:617:34) at Module.eval (file:///home/projects/vitejs-vite-vlfdrkbx/node_modules/rolldown/dist/shared/binding-BMtuZhvP.mjs:25:49) at eval (file:///home/projects/vitejs-vite-vlfdrkbx/node_modules/rolldown/dist/shared/parse-B7LUicIJ.mjs:19:84) 解決方法 1. ``package.json` の `vite` を `^7.1.0`` に変更 1. ``rm -rf package-lock.json`` を実行 1. ``npm install`` を実行 1. ``npm run dev`` を実行 おわりに Vite8の内部ビルドツールがstackblitzで起動できないエラーが起きていたようです。まさかここでつまづくと思ってなかったけど解決できてよかったです。

    2 likes
  5. Zenn · · @hii_chi · npm/cli

    コードに紛れ込んだパスワードとメールアドレスを、コミット前に Betterleaks で止める GitHub Free の個人アカウントでは、プライベートリポジトリにシークレットスキャンを使えません。そこで、Betterleaks を pre-commit フックで動かし、コミットの手前で止めることにしました。ところが、デフォルトのルールでは、password = "..." のような英語の書き方は止まりますが、「パスワードは … です。」という日本語のコメントは通ってしまいます。この記事では、日本語のルールと awk のチェックを足して、パスワード・メールアドレス・実名入りのパスを止める構成を紹介します。 結論 Betterleaks を pre-commit フックで動かし、2 つのチェックを足します。 日本語の「パスワードは xxx」を拾う Betterleaks のルール コミットで追加するコードの行から、メールアドレスと、ユーザー名の入ったパス(/home/taro など)を探す awk のチェック コードに次のような行を書いてコミットしたときの結果です。 コードに書いた行 Betterleaks のデフォルトのルールだけ ルールとチェックを足したあと const password = "Zq8vR2mXw4Lp9TnK"; 止まる 止まる // パスワードは Zq8vR2mXw4Lp9TnK です。 止まらない 止まる // パスワードは自分で決めます。 止まらない 止まらない const reportTo = "someone@mail.invalid"; 止まらない 止まる const sshDir = "/home/taro/.ssh"; 止まらない 止まる このリポジトリでは、devcontainer(VS Code の Dev Containers で使う開発用のコンテナ)の中で開発し、コミットもコンテナの中で行います。Betterleaks もコンテナに入れます。使うファイルは次の 4 つです。 ファイル やること .devcontainer/Dockerfile Betterleaks をイメージに入れる .githooks/pre-commit ステージした変更を Betterleaks と awk で調べ、見つかればコミットを止める .betterleaks.toml 日本語のパスワードのルールを足す .devcontainer/devcontainer.json git config core.hooksPath .githooks でフックを有効にする なお、日本語の keywords を付けると、ルールが動かなくなりました。詳しくは手順 3 で説明します。 はじめに なぜこの仕組みを作ったか 目的は、パスワードなどの機密情報をリポジトリに入れないことです。 GitHub には、リポジトリに入った秘密の値を見つけるシークレットスキャンと、push の時点で止める push protection があります。ただ、About secret scanning によると、シークレットスキャンが無料で動くのはパブリックリポジトリです。個人アカウントのリポジトリで使えるのは、GitHub Enterprise Cloud の Enterprise Managed Users などの場合に限られます。Push protection も、ユーザー単位のものはデフォルトで有効ですが、対象はパブリックリポジトリです。リポジトリ単位のものは、GitHub Secret Protection が必要です。GitHub Free の個人アカウントのプライベートリポジトリでは、どちらも使えません(2026 年 10 月時点)。そこで、代わりになる方法を探していました。 コードの中には、メールアドレスやパスワードなどが紛れ込むことがあります。そうしたときに備えて、コミットの手前で止める仕組みを自分で用意しました。 選んだもの Gitleaks ではなく Betterleaks: Gitleaks の README には「Gitleaks is feature complete」とあり、今後のリリースはセキュリティの修正だけで、作者は Betterleaks に軸を移すと書かれています。Betterleaks は、元の作者を含む、Gitleaks を作った人たちがメンテナンスしています。 Husky ではなく .githooks/: 個人開発で、このリポジトリを複数人で使うことはないので、Husky は使いませんでした。また、このリポジトリでは devcontainer を作るときに、セキュリティ対策として npm install --ignore-scripts を実行しています。Husky は package.json の prepare スクリプトでフックを設定しますが、--ignore-scripts では package.json のスクリプトが実行されないので、Husky を入れてもフックは設定されません。 検証環境 ベースイメージ:node:24-trixie Betterleaks:1.9.0 Git:2.47.3 Docker:29.9.2 Docker Compose:v5.6.1 手順 1:Betterleaks をイメージに入れる .devcontainer/Dockerfile に、次の RUN を足します。バージョンを固定し、リリースの checksums.txt で SHA256 を確かめてから /usr/local/bin に展開します。 .devcontainer/Dockerfile Install Betterleaks バージョンを固定し、リリースの checksums.txt で SHA256 を検証してからインストールする ARG BETTERLEAKS_VERSION=1.9.0 RUN ARCH="$(dpkg --print-architecture)" \ && case "$ARCH" in amd64) ARCH=x64 ;; esac \ && cd /tmp \ && curl -fsSLO "github.com/betterleaks/betterleaks/releases/downl…" \ && curl -fsSLO "github.com/betterleaks/betterleaks/releases/downl…" \ && grep " betterleaks_${BETTERLEAKS_VERSION}_linux_${ARCH}.tar.gz$" checksums.txt | sha256sum -c - \ && tar -xzf "betterleaks_${BETTERLEAKS_VERSION}_linux_${ARCH}.tar.gz" -C /usr/local/bin --no-same-owner betterleaks \ && rm -f "betterleaks_${BETTERLEAKS_VERSION}_linux_${ARCH}.tar.gz" checksums.txt v1.9.0 の Linux 向けのファイル名は betterleaks_1.9.0_linux_x64.tar.gz と betterleaks_1.9.0_linux_arm64.tar.gz です。x86_64 の環境では dpkg --print-architecture が amd64 を返すので、x64 に置き換えています。 手順 2:.githooks/pre-commit を書き、core.hooksPath で有効にする フックの全文です。awk の部分は手順 4 で説明します。 .githooks/pre-commit #!/usr/bin/env bash ステージした変更に機密情報があればコミットを止める。 1. Betterleaks: リポジトリ全体の staged diff からトークン・パスワード・API キーなどを探す 2. 補助チェック: リポジトリ全体の追加行からメールアドレスと実名入りのホームのパスを探す 誤検出なら、内容を確かめたうえで git commit --no-verify で通す。 set -uo pipefail if ! command -v betterleaks >/dev/null 2>&1; then echo "pre-commit: betterleaks が見つかりません。" >&2 exit 1 fi failed=0 --verbose で場所を出し、--redact で秘密の値は伏せる。--validation(提供元への問い合わせ)は付けない .betterleaks.toml(日本語のパスワードのルールを足している)は --config で渡す。環境変数 BETTERLEAKS_CONFIG などより優先させ、ファイルがなければエラーで止めるため if ! betterleaks git . --staged --no-banner --redact --verbose --config "$(git rev-parse --show-toplevel)/.betterleaks.toml"; then failed=1 fi 追加行だけを見る(既存の行は再検出しない)。"+++ b/<path>" からファイル名を拾う core.quotepath=false: 日本語のファイル名を "\351\200..." のようにせず、そのまま出す --diff-filter=ACMR: 名前の変更(R)やコピー(C)として扱われたファイルも見る hits=$(git -c core.quotepath=false diff --cached -U0 --diff-filter=ACMR | awk ' /^+++ / { file = substr($0, 7); next } /^@@/ { split($3, a, /[+,]/); line = a[2]; next } /^+/ { s = substr($0, 2) t = s 例外にする文字列を取り除いてから探す 予約済みの例示用ドメイン gsub(/[A-Za-z0-9._%+-]+@example.(com|org|net)/, "", t) devcontainer の実行ユーザー(node)のホーム gsub(/\/home\/node([^A-Za-z0-9._-]|$)/, " ", t) Windows の公開フォルダー(Public)のデフォルトのパス gsub(/(C:\Users\|\/mnt\/c\/Users\/)Public([^A-Za-z0-9._-]|$)/, " ", t) if (t ~ /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(.[A-Za-z0-9-]+)*.[A-Za-z][A-Za-z]+/ || t ~ /\/home\/[A-Za-z0-9._-]/ || t ~ /(C:\Users\|\/mnt\/c\/Users\/)[A-Za-z0-9._-]/) printf " %s:%d: %s\n", file, line, s line++ } ') if [ -n "$hits" ]; then echo "pre-commit: 追加行にメールアドレスか実名入りのパスがあります。" >&2 echo "$hits" >&2 failed=1 fi if [ "$failed" -ne 0 ]; then echo "pre-commit: コミットを止めました。確かめたうえで問題なければ git commit --no-verify で通せます。" >&2 exit 1 fi Betterleaks の呼び出しで使っているオプションは次のとおりです。説明は v1.9.0 の betterleaks git --help のもの(先頭の 1 文)です。 オプション --help の説明 --staged scan staged commits (good for pre-commit) --redact redact secrets from logs and stdout --verbose show verbose output from scan --config config file path --redact を付けると、見つかった値はターミナルの出力でも REDACTED に置き換わります。見つかった値を提供元の API に問い合わせて確かめる --validation(enable validation of findings against live APIs)は付けていません。 --config を付けている理由は、手順 3 で説明します。 スクリプトは set -e を付けずに、見つかったら failed=1 にして最後まで進めます。こうすると、Betterleaks と awk の両方で見つかったときに、1 回のコミットで両方の結果がまとめて出ます(後の「動作確認」を参照)。 最初の if は、betterleaks コマンドがなければコミットを止めます。PATH から betterleaks を外してコミットすると、次のように止まりました。 pre-commit: betterleaks が見つかりません。 フックを有効にする Git のフックの置き場所は、デフォルトでは .git/hooks/ です。githooks によると、この場所は core.hooksPath で変えられます。.githooks/ に置いて core.hooksPath で指定すれば、フックをリポジトリと一緒に管理できます。 このリポジトリでは、devcontainer を作るときに実行する postCreateCommand に、次の 1 行を入れています(devcontainer.json の該当部分だけを抜き出しています)。 git config core.hooksPath .githooks また、githooks には「Hooks that don't have the executable bit set are ignored.」とあるので、フックには実行権限を付けておきます。 chmod +x .githooks/pre-commit 手順 3:日本語のパスワードのルールを足す デフォルトのルールでは日本語のパスワードが止まらない .betterleaks.toml を置かずに、次のファイルをステージして Betterleaks を実行してみました。 src/t1.js // test const password = "Zq8vR2mXw4Lp9TnK"; // パスワードは Zq8vR2mXw4Lp9TnK です。 // パスワード: Hk3mPq7Ld9WxRt2B 止まったのは、英語の password の 2 行目だけでした。日本語のコメントの 3 行目と 4 行目は止まりませんでした。 ┌─generic-password──○ │ │ 2 │ const password = "REDACTED"; │ │ ^^^^^^^^ │ │ attributes: │ confidence ....... low │ git.message ...... │ git.sha .......... │ path ............. src/t1.js │ resource ......... git.patch_content └○ 8:51PM INF scanned ~131 bytes (131 bytes) in 105ms 8:51PM WRN leaks found: 1 .betterleaks.toml 日本語の文中の「パスワードは xxx」を拾うルールを足します。 .betterleaks.toml .githooks/pre-commit が --config で読む Betterleaks の設定。 デフォルトのルールは英語の password / token などを手がかりにするので、日本語の文中の「パスワードは xxx」を足す。 [extend] useDefault = true 値は ASCII の英数字と記号(空白と引用符を除く)の 4 文字以上だけを拾う(「パスワードは自分で決めます」「8 文字以上」「<パスワード>」は対象外)。 keywords は付けない。日本語のキーワードを付けると、事前の絞り込みで一致せずルールが動かない(v1.9.0 で確認)。 [[rules]] id = "ja-password" description = "日本語の文中に書かれたパスワード・パスフレーズ・暗証番号" regex = '''(?:パスワード|パスフレーズ|暗証番号)\s*(?:は|が|:|:|=)\s*[`"'「]?([A-Za-z0-9!#$%&()*+,./:;<=>?@[]^_{|}~\-]{4,})''' [[rules]] の下の id・description・regex が、ルール 1 つ分の設定です。id は、ルールを区別する名前です(docs/config.md の「id: unique rule identifier.」)。ja-password は、日本語のパスワードを拾うルールとしてこの設定で付けた名前です。このルールで見つかると、出力の先頭に ┌─ja-password──○ と表示されます。デフォルトのルールで止まったときの generic-password も、同じくルールの id です。 [extend] の useDefault = true で、デフォルトのルールを残したまま ja-password を足しています。 ルールは「パスワード・パスフレーズ・暗証番号」の後に「は・が・:・:・=」が続き、その後に ASCII の英数字と記号(空白と引用符を除く)が 4 文字以上続くと一致します。値を ASCII に絞ったのは、手順を説明する普通の文章で止まらないようにするためです。実際にコミットした結果は次のとおりです。 コードに書いた行 結果 // パスワードは Zq8vR2mXw4Lp9TnK です。 止まる // パスワード: Hk3mPq7Ld9WxRt2B 止まる // パスフレーズ:Hk3mPq7Ld9 止まる // パスワードは自分で決めます。 止まらない // パスワードは 8 文字以上にします。 止まらない // ssh のパスワードは <パスワード> に置き換えます。 止まらない // 暗証番号が 1234 だと弱いです。 止まる(誤検出) // パスワードを Zq8vR2mX にします。 止まらない 最後の 2 行は、このルールの限界です。「暗証番号が 1234 だと弱いです。」は本物の暗証番号ではありませんが、「が」の後に数字が 4 文字続くので止まります。このように止まったときは、内容を確かめたうえで git commit --no-verify で通します。「パスワードを xxx にします。」のように、「は・が」などの後に値が来ない書き方は拾いません。 日本語の keywords を付けるとルールが動かない docs/config.md の例のように keywords を付けたくなりますが、日本語の keywords を付けると、かえってこのルールが動かなくなります。 Betterleaks のルールには、正規表現の前の絞り込みに使う keywords も書けます。英単語と同じように日本語でも機能すると思い、ja-password に日本語の keywords を付けたところ、v1.9.0 では ja-password で 1 件も止まらなくなりました。 手順 3 の最初で使った src/t1.js と、上の表の行を書いたファイルの 2 つをステージしたまま実行し直しました。.betterleaks.toml の ja-password のルールでは、regex の前に次の 1 行を足しています。 keywords = ["パスワード", "パスフレーズ", "暗証番号"] keywords がないときは ja-password で 4 件止まりましたが、付けると 0 件になり、英語の generic-password の 1 件だけが残りました。 generic-password src/t1.js 2 password = "REDACTED"; v1.9.0 のソースを読むと、keywords は正規表現を試す前の絞り込みに使われています。スキャンする内容の中に keywords のどれかがあるルールだけが、正規表現まで進みます。この絞り込みは、日本語の文字のような ASCII(半角の英数字と記号)以外の文字を読み飛ばしていました(internal/ahocorasick/matcher.go)。そのため、日本語の keywords は一致せず、ルールが候補から外れます。keywords を付けなければ、ルールは毎回正規表現まで進むので、日本語でも一致します。 .betterleaks.toml は --config で渡す フックでは、.betterleaks.toml を --config "$(git rev-parse --show-toplevel)/.betterleaks.toml" で渡しています。環境変数 BETTERLEAKS_CONFIG などより優先させ、ファイルがなければエラーで止めるためです。 betterleaks git --help によると、設定ファイルは --config、環境変数(BETTERLEAKS_CONFIG など)、スキャン対象のディレクトリの .betterleaks.toml の順で探されます。githooks によると、Git はフックを動かす前に作業ディレクトリのルートに移動します。そのため、--config がなくても、普通にコミットする分には .betterleaks.toml は読まれます。--config のあり・なしで試した結果は、次の表のとおりです。差が出たのは、2 行目と 3 行目の場面でした。 場面 --config あり --config なし ルート・サブディレクトリ・リポジトリの外(git -C)からコミット ja-password が効く ja-password が効く 環境変数 BETTERLEAKS_CONFIG か GITLEAKS_CONFIG に別の設定ファイルがある ja-password が効く ja-password が効かない .betterleaks.toml がない Betterleaks がエラーで終わり、コミットが止まる デフォルトのルールだけで動き、ja-password が効かない 手順 4:メールアドレスと実名入りのパスは awk で見る メールアドレスと実名入りのホームのパスは、フックの後半の awk で探しています。 git -c core.quotepath=false diff --cached -U0 --diff-filter=ACMR で、ステージしたファイルの差分を取ります。+ で始まる追加行だけを調べるので、すでにある行は検出し直しません。 Git のドキュメント によると、--diff-filter=ACMR は追加(Added)・コピー(Copied)・変更(Modified)・名前の変更(Renamed)のファイルだけに絞るオプションで、-U0 は前後の行(コンテキスト)の数を 0 にするオプションです。 -c core.quotepath=false は、日本語のファイル名をそのまま出すための設定です。Git のドキュメント によると、core.quotePath のデフォルトは true です。このとき、0x80 より大きいバイト(日本語の文字など)を含むファイル名は、ダブルクォートで囲まれ、\302\265 のような 8 進数で出力されます。false にすると、そのまま出力されます。この設定を付けずに src/連絡先.js で試すと、止まったときの表示は次のように文字化けしました。 /src/\351\200\243\347\265\241\345\205\210.js":1: const reportTo = "someone@mail.invalid"; -c core.quotepath=false を付けると、src/連絡先.js:1: と表示されます。 C と R を足している理由は、後の「検証していて見つけた穴:名前を変えたファイルとコピーしたファイル」で説明します。 awk の処理の詳しい説明 後の「動作確認」の src/sample.js をステージしたときの差分は、次のとおりです。 diff --git a/src/sample.js b/src/sample.js new file mode 100644 index 0000000..587d80c --- /dev/null +++ b/src/sample.js @@ -0,0 +1,8 @@ +// テスト用のコード + +// 検証用ユーザーのパスワードは Zq8vR2mXw4Lp9TnK です。 +const reportTo = "someone@mail.invalid"; +const example = "user@example.com"; + +const sshDir = "/home/taro/.ssh"; +const workDir = "/home/node/app"; awk はこの差分を 1 行ずつ読み、行の先頭の文字で処理を分けます。 awk の処理 対象の行 やっていること /^+++ / { file = substr($0, 7); next } +++ b/src/sample.js 7 文字目から後ろ(+++ b/ の 6 文字を除いた部分)を、ファイル名として file に入れる /^@@/ { split($3, a, /[+,]/); line = a[2]; next } @@ -0,0 +1,8 @@ 3 つ目の欄(+1,8)を + と , で区切り、追加された行の最初の行番号(1)を line に入れる /^+/ { ... } - で始まる追加行 下の 1〜4 を行う。+++ の行は、その前の処理の next で次の行に進むので、ここには来ない 追加行に対しては、次の順に処理します。 s = substr($0, 2):先頭の + を除いた行の中身を s に入れます。見つかったときの表示には、これを使います。 t = s と gsub(...):調べるためのコピー t を作り、gsub で、例外にする文字列を取り除きます。メールアドレスは空文字に、パスは後ろの区切りの文字ごと空白 1 つに置き換えます。 if (t ~ ... || ...):例外にする文字列を取り除いた t に、メールアドレス・/home/<名前>・C:\Users<名前> か /mnt/c/Users/<名前> のどれかが残っていれば、printf で「ファイル名:行番号: 元の行」を出します。 line++:見つかったかどうかに関係なく、次の追加行のために行番号を 1 つ進めます。 awk が出したものは、hits=$(...) で変数 hits に入ります。hits が空でなければ([ -n "$hits" ])、メッセージと一緒に表示して failed=1 にします。 awk では、探す前に、止めなくてよいメールアドレスやパスを行から取り除いています。この記事では、これを「例外」と呼びます。例外にした文字列は調べる対象から外れますが、同じ行のほかの部分は調べます。 例外にするパスは、後ろに区切りの文字が続くか、行末のときだけ取り除きます。そのため /home/node/app は例外になりますが、/home/nodejs/app は例外にならずに止まります。 例外にしているのは、次のものです。 例外 理由 @example.com・@example.org・@example.net のアドレス 例示用のドメインだから。RFC 2606 で、例として使えるドメインとして予約されている /home/node devcontainer の実行ユーザーだから C:\Users\Public と /mnt/c/Users/Public 個人の名前ではないから。Windows の公開フォルダー(Public)のデフォルトのパスが %SystemDrive%\Users\Public 動作確認 次のファイルをステージしてコミットしました。 src/sample.js // テスト用のコード // 検証用ユーザーのパスワードは Zq8vR2mXw4Lp9TnK です。 const reportTo = "someone@mail.invalid"; const example = "user@example.com"; const sshDir = "/home/taro/.ssh"; const workDir = "/home/node/app"; Betterleaks がパスワードを、awk がメールアドレスと実名入りのパスを見つけ、1 回のコミットで両方の結果がまとめて表示されました。例外にしている user@example.com と /home/node は止まっていません。 ┌─ja-password──○ │ │ 3 │ // 検証用ユーザーのパスワードは REDACTED です。 │ │ ^^^^^^^^ │ │ attributes: │ git.message ...... │ git.sha .......... │ path ............. src/sample.js │ resource ......... git.patch_content └○ 10:38PM INF scanned ~248 bytes (248 bytes) in 31.1ms 10:38PM WRN leaks found: 1 pre-commit: 追加行にメールアドレスか実名入りのパスがあります。 src/sample.js:4: const reportTo = "someone@mail.invalid"; src/sample.js:7: const sshDir = "/home/taro/.ssh"; pre-commit: コミットを止めました。確かめたうえで問題なければ git commit --no-verify で通せます。 同じ行に、例外と本物のメールアドレスやパスが混ざる場合も試しました。次のファイルをステージしてコミットしました。 src/mixed.js const to = ["user@example.com", "someone@mail.invalid"]; const dirs = ["/home/node/app", "/home/taro/.ssh"]; const only = ["user@example.com", "/home/node/app"]; 例外と本物が同じ行にある 1・2 行目は止まり、例外だけの 3 行目は止まりませんでした。 10:00PM INF scanned ~162 bytes (162 bytes) in 31.4ms 10:00PM INF no leaks found pre-commit: 追加行にメールアドレスか実名入りのパスがあります。 src/mixed.js:1: const to = ["user@example.com", "someone@mail.invalid"]; src/mixed.js:2: const dirs = ["/home/node/app", "/home/taro/.ssh"]; pre-commit: コミットを止めました。確かめたうえで問題なければ git commit --no-verify で通せます。 問題のないコードは、そのままコミットできました。 10:38PM INF scanned ~57 bytes (57 bytes) in 30.8ms 10:38PM INF no leaks found 検証していて見つけた穴:名前を変えたファイルとコピーしたファイル この記事のために検証していて、名前を変えたファイルが awk のチェックを素通りすることに気づきました。 最初は --diff-filter=AM にしていました。20 行の old.js をコミットしたあと、git mv で new.js に名前を変え、メールアドレスの行を足してステージしました。Git はこのファイルを名前の変更(R)として扱います。 $ git diff --cached --name-status R089 old.js new.js --diff-filter=AM ではこのファイルの差分が出ないので、awk は何も見ません。Betterleaks の結果だけが出て、コミットはそのまま通りました。 9:07PM INF scanned ~41 bytes (41 bytes) in 31.8ms 9:07PM INF no leaks found Betterleaks のほうは、名前を変えたファイルの追加行も調べます。同じように名前を変えて日本語のパスワードの行を足すと、ja-password で止まりました。穴があったのは awk のチェックだけです。 --diff-filter=AMR にすると、名前を変えたファイルの差分も awk に渡ります。続けて、素通りしたメールアドレスの行を含む new.js(21 行)を new3.js に名前を変え、別のメールアドレスの行を足してコミットしました。止まったのは、足した 22 行目だけです。すでにあった 21 行目は検出し直しません(awk の部分を抜粋)。 pre-commit: 追加行にメールアドレスか実名入りのパスがあります。 new3.js:22: const reportTo = "other@mail.invalid"; コピーしたファイルでも、同じことが起きる場合があります。Git のドキュメント によると、diff.renames のデフォルトは true で、名前の変更だけを見つけます。copies にすると、コピーも見つけます。 diff.renames=copies の設定で、20 行の old.js を copy.js にコピーしてメールアドレスの行を足し、old.js にも 1 行足してステージすると、copy.js はコピー(C)として扱われます。diff-options.txt の --find-copies-harder の説明によると、コピーとして見つかるのは、デフォルトではコピー元も同じ変更の中で変更されているときだけです。 $ git -c diff.renames=copies diff --cached --name-status C089 old.js copy.js M old.js この状態で --diff-filter=AMR のフックを使うと、メールアドレスの行は止まらずにコミットされました。--diff-filter=ACMR にすると、足した行で止まりました。デフォルトの設定ではコピーは追加(A)として扱われるので、AMR でも止まります。設定によらず止まるように、フックは --diff-filter=ACMR にしています。 この構成で防げないこと githooks にあるとおり、pre-commit フックは git commit --no-verify で飛ばせます。このフックでも、誤検出のときは --no-verify で通す運用にしています。 --staged でステージした変更だけを調べるので、すでにコミット済みの履歴は調べません。 日本語のルールは「は・が・:・:・=」の後に値が来る書き方だけを拾います。「パスワードを xxx にします。」のような書き方は拾いません。 「暗証番号が 1234 だと弱いです。」のように、本物でなくても止まることがあります。 まとめ Betterleaks のデフォルトのルールでは、英語の password は止まっても、日本語の「パスワードは xxx」は止まりませんでした。日本語でコメントを書くリポジトリでは、日本語のルールを足します。 日本語の keywords を付けると、v1.9.0 ではルールが動かなくなりました。keywords は付けずに使います。 メールアドレスと実名入りのパスは、awk で追加行を調べます。--diff-filter に C と R を入れないと、名前を変えたファイルやコピーしたファイルに足した行が素通りすることがあります。 誤検出で止まったときは、内容を確かめたうえで git commit --no-verify で通します。

  6. DEV · · @kachurun · npm/cli

    NPM Is Totally Compromised. How Do You Run `npm install` Safely Now? You rarely think about what code lands on your machine during a routine npm install, or what it might do once it gets there. You expect build errors or version conflicts. A compromised package can steal your SSH keys, GitHub tokens, AWS credentials, and access to crypto wallets—and then use your credentials to infect your own packages. A malicious release can turn up in a library you’ve used for years, caught in a chain of compromises spreading through other packages. September and October 2026 alone gave us plenty of examples: - SubQuery. Researchers found credential theft and remote command execution code in @subql/common@5.8.3. The package could reach a project through other SubQuery dependencies. StepSecurity’s analysis. - Tensorlake. Version tensorlake@0.5.144 stole tokens, tried to spread to other packages, and installed a background process that deleted the user’s home directory if the stolen GitHub token was revoked. StepSecurity’s analysis. - MemTensor. Malicious releases of an AI plugin harvested credentials. During memory recall, the plugin also passed the user’s prompt to a hidden process. StepSecurity’s analysis. Meanwhile, attacks from the Shai-Hulud family continued. Its approach is particularly unpleasant for package maintainers: the worm steals a publishing token, injects malicious code into packages the victim can publish, and releases new versions under their name. Unit 42 explains the mechanism and subsequent campaigns. Earlier in 2026, even Axios was compromised. Malicious releases pulled in an additional dependency that installed a remote access trojan. Socket’s analysis. TanStack packages were hit too. Compromised releases included code targeting CI credentials and a Git dependency that ran a malicious script through its prepare hook. Socket’s separate analysis. Your package.json might not change at all: { "dependencies": { "some-package": "^1.2.3" } } That range allows newer versions considered compatible under semver, such as 1.3.4. If the package manager selects a compromised release the next time it resolves dependencies, a routine installation becomes an entry point for an attack. npm and other package managers need to rethink how we work with dependencies. In the meantime, there are several things you can configure to reduce the risk on your machines and in CI. 1. Give new releases a cooldown period The first setting I’d enable is a delay before installing newly published releases. We’ll use a week in the examples: if a version was published less than seven days ago, leave it alone for now. Personally, I think three days would be enough too. That gives researchers and maintainers time to spot a compromised release, warn users, and get it removed from the registry. For npm 11.10.0+: # .npmrc min-release-age=7 The value is in days. This option was introduced in npm 11.10.0. If no available version meets the age requirement and the dependency constraints, installation fails. Configuration reference. For pnpm 11+: # pnpm-workspace.yaml minimumReleaseAge: 10080 minimumReleaseAgeStrict: true Here, the value is in minutes: 10080 is seven days. Setting minimumReleaseAgeStrict: true prevents pnpm from choosing a newer release even when no older version satisfies the dependency constraints. pnpm 11 has a one-day delay by default, but without strict mode it can fall back to a release that is too new. Set the policy explicitly. pnpm documentation. For Yarn 4.12+: # .yarnrc.yml npmMinimalAgeGate: "1w" Yarn also enables a one-day delay by default. This increases it to a week. Yarn documentation. For Bun 1.3+: # bunfig.toml [install] minimumReleaseAge = 604800 Bun uses seconds: 604800 is seven days. The setting was introduced in Bun 1.3. There’s a catch: Bun applies the filter when resolving new versions. Dependencies already recorded in bun.lock stay unchanged, so adding this setting won’t retroactively check your entire lockfile. How the filter works. Waiting a week doesn’t make a package safe. It does reduce the chance of installing a malicious release immediately after publication, before anyone has raised the alarm. An urgent security fix may need to go in sooner. Make that exception for a specific update after reviewing it. 2. Restrict automatic install scripts Packages can run commands through preinstall, install, and postinstall. Some dependencies have legitimate reasons for doing this, such as building native modules or downloading binaries. Those commands run with the permissions of the user who started the installation. If that user can access SSH keys, tokens in .npmrc, or secrets in environment variables, a script potentially can too. For npm, you can disable automatic scripts entirely: # .npmrc ignore-scripts=true This disables automatic script execution during installation. Explicit commands such as npm run build still work. npm documentation. Starting with npm 12, dependency install scripts are blocked by default unless the allowScripts policy permits them. So the claim that npm always runs every package’s scripts no longer applies to newer versions. npm 12 release notes. With pnpm 11+, third-party dependency scripts also require approval. You can configure exceptions in pnpm-workspace.yaml: # pnpm-workspace.yaml allowBuilds: esbuild: true core-js: false This illustrates the syntax; approve a package only after reviewing its script. Dependencies that aren’t listed are treated as unreviewed. With the default settings, pnpm fails the installation if they require a build. To disable installation scripts for both the project and its dependencies: # pnpm-workspace.yaml ignoreScripts: true With this setting enabled, an allowBuilds entry won’t override the ban. Choose whether to disable all scripts or allow specific dependencies. pnpm documentation. For Yarn 4: # .yarnrc.yml enableScripts: false This has been the default since Yarn 4.14. The setting restricts scripts from third-party packages; scripts in your own workspaces still run. You can allow individual dependencies through dependenciesMeta. Yarn documentation. For Bun, save the setting in the project configuration: # bunfig.toml [install] ignoreScripts = true If you’ve already added minimumReleaseAge, put both settings under the same [install] section: # bunfig.toml [install] minimumReleaseAge = 604800 ignoreScripts = true Bun restricts dependency scripts even without this setting, but it has a built-in list of trusted packages. Saying that Bun doesn’t run install scripts by default would therefore be inaccurate. If you need selective approvals instead of a blanket ban, use trustedDependencies in package.json. An empty list removes trust from all dependencies, including the built-in exceptions: { "trustedDependencies": [] } You can add reviewed packages to that list. With ignoreScripts = true, scripts remain disabled regardless of which packages you trust. Bun documentation. Some dependencies may stop installing or working after you change these settings. You’ll need to check why they require a script and decide whether to allow it. Restricting install scripts covers only part of the problem. Malicious code can still run during a normal import, a build, or application execution. In the MemTensor incident, execution was built into the plugin’s normal operation. 3. Commit your lockfile and use it in CI Keep the lockfile in the repository alongside your code. It records specific versions of both direct and transitive dependencies. In CI, use the command for your package manager: | Package manager | Lockfile | Command | | --- | --- | --- | | npm | package-lock.json | npm ci | | pnpm | pnpm-lock.yaml | pnpm install --frozen-lockfile | | Yarn 4 | yarn.lock | yarn install --immutable | | Bun | bun.lock | bun ci | These commands install from the lockfile and fail if it needs to change to match package.json. See the documentation for npm, pnpm, Yarn, and Bun. For Bun, bun ci is equivalent to bun install --frozen-lockfile. Make sure you commit bun.lock: a regular bun install doesn’t automatically enable frozen mode just because it’s running in CI. You can also remove ^ and ~ from direct dependencies, but that doesn’t replace a lockfile. Libraries have their own dependencies. A lockfile doesn’t check for malware either. If a compromised version is already in it, a reproducible installation will keep installing that version. Handle dependency updates in separate PRs so you can review which packages and versions are changing. Building your project shouldn’t also decide that it’s time to update dependencies. You can save exact versions by default, so adding a dependency writes "1.2.3" to package.json instead of "^1.2.3". For npm, add this to .npmrc: save-exact=true New dependencies will be saved without a version range. npm documentation. For pnpm 11+, add this to pnpm-workspace.yaml: saveExact: true pnpm documentation. For Yarn 4, add this to .yarnrc.yml: defaultSemverRangePrefix: "" The empty string disables the automatic addition of ^ or ~. Yarn documentation. For Bun, add exact to the existing [install] section in bunfig.toml: [install] exact = true Bun documentation. These settings change how new dependencies are saved. They won’t automatically rewrite existing entries containing ^ or ~. Pinning fixes the version of a direct dependency, but that package’s own dependencies may still use ranges. You still need a lockfile and frozen installs in CI. Pinned versions can then be updated through separate PRs, with the changes reviewed before merging. 4. Check dependency updates for malware npm audit is useful for finding known vulnerabilities, but don’t rely on it alone. A version published yesterday that steals tokens today may not have a CVE or an entry in a conventional vulnerability database yet. Tools such as Socket, Aikido Security, and StepSecurity can check dependencies for malicious code and suspicious behavior. StepSecurity, for example, has separate checks for known compromised versions and releases that are too new, described in its Tensorlake analysis. Run these checks on dependency update PRs before merging. That gives you a warning while you can still stop the update. 5. Keep secrets out of jobs that install dependencies Look at the permissions and secrets available to the process running npm install. Does it need production credentials? Permission to publish packages? A token with write access to every repository? The less access that process has, the less a malicious dependency can steal or change. Separate builds from deployments, pass secrets only to the jobs that need them, and limit GITHUB_TOKEN permissions to the task at hand. Pin third-party GitHub Actions to full commit SHAs too. A moving tag can start pointing to different code while your workflow file stays exactly the same. Attacks through CI are already happening at scale. In October 2026, another GhostAction wave hit more than 30,000 GitHub repositories, injecting workflows designed to steal secrets. GitGuardian’s investigation. Final configs Save these settings explicitly in your project so installation behavior doesn’t depend on whichever defaults a package manager release happens to choose. These configs set a one-week delay, disable automatic install scripts, and save exact versions for new dependencies. npm 11.10.0+ — .npmrc: # Minimum release age: 7 days min-release-age=7 # Don't run scripts during installation ignore-scripts=true # Save exact versions without ^ or ~ save-exact=true npm documentation. pnpm 11+ — pnpm-workspace.yaml: # Minimum release age: 7 days, in minutes minimumReleaseAge: 10080 # Fail if no eligible version is available minimumReleaseAgeStrict: true # Don't run scripts during installation ignoreScripts: true # Save exact versions without ^ or ~ saveExact: true Configuration reference: release age, scripts, and saving versions. Yarn 4.12+ — .yarnrc.yml: # Minimum release age: 7 days npmMinimalAgeGate: "1w" # Don't run install scripts from third-party dependencies enableScripts: false # Save exact versions without ^ or ~ defaultSemverRangePrefix: "" This doesn’t disable scripts in your own workspaces. Yarn documentation. Bun 1.3+ — bunfig.toml: [install] # Minimum release age: 7 days, in seconds minimumReleaseAge = 604800 # Don't run scripts during installation ignoreScripts = true # Save exact versions without ^ or ~ exact = true Configuration reference: release age, scripts and exact versions. The npm, pnpm, and Bun examples disable install scripts entirely. If a dependency needs a build step, configure selective approvals through your package manager instead of the blanket ban. Saving exact versions also won’t rewrite existing entries containing ^ or ~. Commit the lockfile alongside the configuration, and use the corresponding command in CI: | Package manager | Lockfile | CI installation | | --- | --- | --- | | npm | package-lock.json | npm ci | | pnpm | pnpm-lock.yaml | pnpm install --frozen-lockfile | | Yarn | yarn.lock | yarn install --immutable | | Bun | bun.lock | bun ci | Malware checks and CI permissions need separate configuration. Keep dependency updates in their own PRs, and pass deployment secrets only to the job that performs the deployment. --- Build your next website with Reforma. Create with AI and take full control with a powerful visual website builder. Design every layout, style every detail, and publish on your own domain.

  7. npm-cliのコードを読んでいるが、`npm innit`と打っても`npm init`として処理してることとかがわかって「へー」となってる npm/cli

    92 views
  8. #javascript #node #webdev By adding a package.json5 file that npm automatically converts into a package.json, you could group your dependencies, add comments about why an npm script was created (or anything)! It would be opt-in, and has NO DOWNSIDES (see post for details) rfcs#291

    2 likes · 1 repost
  9. Three gates before your first npm publish: 2FA, provenance, and staged releases I published a small plugin to npm for the first time last week. It is a DeepSeek Harness plugin called dsh-img2img-config — a composer image editor plus a provider settings panel. The code was done in a day; getting it onto the registry took three separate attempts, each blocked by a different gate. None of the errors said what the gate was for. They said things like EUSAGE, E403, and 404 Not Found — the last one while publishing a package that did not exist yet. This post is the write-up I wish I had found first. Gate 1 — 2FA is a browser flow, not a code The account had 2FA set up with a security key, not a TOTP app. npm publish does not ask for a six-digit code in that case, so the first attempt looked like this: npm error code EOTP npm error This operation requires a one-time password. npm error Open this URL in your browser to authenticate: npm error https://www.npmjs.com/auth/cli/*** The URL is the actual gate. Open it in a browser that is already logged in to npm, confirm with the security key, and the waiting CLI continues. Two things make this confusing: 1. The URL is masked in non-interactive output. Run npm publish from a TTY and the prompt is obvious. Run it from a script, a background job, or an agent, and you get *** and no way to complete the flow. If you are automating, you cannot be the thing that authorizes a new package by hand — that is what trusted publishing is for (Gate 2). 2. An expired session token fails with 404, not 401. After the token in ~/.npmrc had been revoked, npm whoami returned E401 Unauthorized, but npm publish returned: npm error 404 Not Found - PUT https://registry.npmjs.org/my-package - Not found For a brand-new package name, the registry does not tell you whether the name is taken or whether you are simply not authenticated. It returns 404 either way. If a publish fails with 404 on a name you know is free, check npm whoami before you start debugging the package. One more detail worth knowing early: npm is tightening bypass-2FA tokens — account changes first, direct publishing later. The deprecation notice points at gh.io. Gate 2 — provenance: true cannot run on your laptop The package had this in package.json: { "publishConfig": { "access": "public", "provenance": true } } Locally that fails immediately: npm error code EUSAGE npm error Automatic provenance generation not supported for provider: null null here means "I am not running inside a supported CI provider, so I have no OIDC identity to sign a build statement with". Provenance is a SLSA attestation: it says this tarball was built by this workflow, from this commit, in this repository. A laptop has no such identity to vouch for. So you have two options, and they are not interchangeable: - Publish locally with --no-provenance. The package goes up without an attestation. Fine for a first version if you just need the name and a working tarball. - Publish from CI with --provenance and permissions: id-token: write. This is the one that produces the "Built and signed on GitHub Actions" badge on the package page. The CI route is not much more work. A minimal workflow: name: publish on: workflow_dispatch: jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write # required for provenance / trusted publishing steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 registry-url: https://registry.npmjs.org - run: npm install -g npm@latest # OIDC publishing needs a recent npm - run: npm publish --access public --provenance With that workflow present in the repository, configure the trusted publisher once: npm trust github my-package \ --file publish.yml \ --repo my-org/my-repo \ --allow-publish npm trust list my-package then shows what you granted: type: github file: publish.yml repository: my-org/my-repo permissions: publish, stage publish From that point on, a workflow_dispatch run publishes with a signed attestation and no long-lived token. That is the whole point: the gate moves from "a human proves presence with a security key" to "a workflow proves identity with OIDC". You can check the result from the registry: curl -s https://registry.npmjs.org/-/npm/v1/attestations/my-package@0.1.0 | jq '.attestations[].predicateType' # "https://github.com/npm/attestation/tree/main/specs/publish/v0.1" # "https://slsa.dev/provenance/v1" Gate 3 — stage-only tokens and the 0.0.0-stage placeholder The first API token I created was allowed to publish, but only to the staging area. For an existing package that is fine — you stage a version, review it, approve it. For a package that has never been published, npm refuses: npm error 403 Forbidden - PUT https://registry.npmjs.org/my-package npm error Cannot publish "my-package": this token can only publish to a staging area, npm error and "my-package" does not exist yet. Create it first with a direct-capable npm error token, then use `npm stage publish`. (E_STAGE_REQUIRED) The message is oddly specific and worth reading twice: the first publish of a name needs a direct publish capability. Staged publishing is a mechanism for subsequent versions. What you get when a staged path does run is a placeholder version: versions: ["0.0.0-stage", "0.1.0"] _dist-tags: { latest: "0.1.0" } 0.0.0-stage is npm's own placeholder for staged publishing — its description literally reads "Temporary package placeholder for staged publishing". Do not unpublish it; it is the name reservation that lets the staged flow exist. Make sure you set latest to your real version: npm dist-tag add my-package@0.1.0 latest Useful commands from this family: npm stage publish, npm stage list, npm stage view <id>, npm stage approve <id>, npm stage reject <id>. The part nobody warns you about: propagation is not instant The CI run printed: npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access npm notice publish Signed provenance statement with source and build information from GitHub Actions + my-package@0.1.0 Green checkmarks, + my-package@0.1.0, done — except the version was not on the registry yet. For a few minutes, GET /my-package/0.1.0 returned 404, latest still pointed at the previous version, and the tarball URL 404'd. npm says as much, one line earlier: npm notice Your package is being processed and may take a few minutes to become available. So if a "successful" publish does not show up immediately, wait and re-check the registry directly instead of re-running the publish: curl -s -o /dev/null -w '%{http_code}\n' https://registry.npmjs.org/my-package/0.1.0 Checklist I now run before publishing anything 1. npm whoami — is the token still alive? 2. Is this the first publish of the name? Then plan for a direct/interactive publish, not a staged one. 3. Does package.json carry a dsh.bundle / framework manifest that the host can actually load? (For DeepSeek Harness plugins, dsh.client alone is not installable — the most common rejection reason on the community list.) 4. Are there guessed upper bounds in peerDependencies? A >=1.0.0 <2.0.0 on a host package becomes a silent refusal on the next major release. See deepseek-ai/deepseek-harness#10776 for the failure mode: the module resolves, apply() never runs, and the host logs nothing. Pin "no upper bounds" with a test rather than a comment. 5. Prefer the trusted-publisher path from day one: workflow + OIDC + --provenance. No stored token, and the supply-chain evidence is a side effect. What actually shipped Three attempts, one evening: - 0.1.0 — published by hand, with browser authorization and no provenance. - 0.1.1 — published from GitHub Actions through the trusted publisher, with a SLSA provenance attestation and a fixed peer range. The gates are not arbitrary. 2FA asks "is a human here?"; provenance asks "which build produced this artifact, and can anyone check?"; staged publishing asks "is this name already owned, and by whom?". Each one is answered by a different mechanism, which is why the first publish feels like three unrelated failures instead of one publishing model. The package: <npmjs.com> The repository: <Dogwind221/img2img-studio>

    1 reaction
  10. Your Coding Agent Added an npm Package. 6 Commands to Vet It Before You Land the Branch - Book: AI That Acts - The series: AI in TypeScript — 5 books, from your first LLM call to agents in production — all five here - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- One line in the package.json diff is a name you have never typed: a small date-parsing library the agent mentions in passing near the end of its summary. The bug is fixed and the tests pass. The lockfile grew by a few hundred lines. You don't know who publishes that package, when it first appeared, or what it runs when it installs. A USENIX Security 2025 paper generated 576,000 code samples with 16 models and found that at least 5.2% of the packages commercial models suggested did not exist (21.7% for open-source models), which leaves made-up names free for anyone to register. And on March 31, 2026 (UTC), two poisoned axios releases went out to a library with over 100 million weekly downloads. Per Trend Micro, "the only change in both poisoned Axios versions is a single new dependency": plain-crypto-js@4.2.1, published 22 minutes before the first of them, whose postinstall script dropped a remote access trojan. A new name in the manifest or the lockfile is the thing to stop at. Six commands cover it: git diff, npm ci, npm explain, npm view, npm pack and npm audit signatures. Run them in the agent's worktree for every package it added, before you land its branch. They work on npm 11 and 12, and I mark where 12 differs. git diff: list every package the branch added Run this from the agent's worktree, with its branch checked out. The three-dot form of git diff shows only what changed since the branch left main: git diff --stat main...HEAD # New names under dependencies or devDependencies git diff main...HEAD -- package.json # Lockfile entries the branch added or moved git diff main...HEAD -- package-lock.json \ | grep -E '^\+ +"node_modules/' # Any of them that runs code at install time git diff main...HEAD -- package-lock.json \ | grep -E '^\+ +"hasInstallScript"' The last grep matters most. In the lockfile, hasInstallScript is "a flag to indicate that the package has a preinstall, install, or postinstall script." If it prints anything, find its entry in the full diff. Expect the lockfile list to be longer, since one direct dependency brings its own. The axios attack lived there: nothing new in your package.json, one new transitive name in the lock. If the agent hasn't committed yet, drop the dots (git diff main -- package.json) to compare your working tree with main. On npm 12, also look for a new allowScripts key in the package.json diff. It lists which dependencies may run install scripts, and npm install-scripts approve writes to it. If the agent approved one, the line is there for you to read. npm ci and npm explain: install without running anything npm ci --ignore-scripts npm explain name-from-the-diff npm ci deletes node_modules and installs exactly what the lockfile says. It "will never write to package.json or package-lock.json," and it exits with an error if the two disagree, which tells you package.json changed without the lockfile being regenerated. ignore-scripts means "npm does not run scripts specified in package.json files," so vetting a package doesn't execute it. npm 12 already blocks unapproved dependency scripts, and this flag also skips any the agent approved. npm explain prints "the chain of dependencies causing a given package to be installed." For a direct dependency the chain ends at your project. For a transitive one it names the parent: on plain-crypto-js it would have pointed at axios, whose source never imported it. npm view: check age, owners and source npm view reads the registry's metadata, and a period selects a child field: PKG=name-from-the-diff VER=version-from-the-lockfile npm view "$PKG" time.created time.modified npm view "$PKG" "time[$VER]" npm view "$PKG" maintainers repository.url npm view "$PKG@$VER" scripts npm view "$PKG@$VER" dist.attestations How to read the answers: - time.created and time[$VER]: a package first published last week, with one or two versions, deserves a slower read. So does a version only minutes old, like plain-crypto-js@4.2.1 when axios started depending on it. - maintainers and repository.url: who can publish, and where the code supposedly lives. The repository field is whatever the publisher typed, so open it and look for a tag matching $VER. Trend Micro found "no commit, tag, or release" in the axios repo for either poisoned version. - scripts: a preinstall, install or postinstall entry is code that runs on your machine at install time. - dist.attestations: whether the version has a provenance attestation. The registry record for axios 1.14.0 carries a SLSA provenance statement. The poisoned 1.14.1 "was published manually with a stolen npm access token, with no OIDC binding and no gitHead." Run the command on the previous version too. An attestation that disappears between releases is a reason to stop. npm pack and npm audit signatures: see what ships npm pack "$PKG@$VER" --dry-run npm audit signatures npm pack accepts a registry name and version, and with dry-run npm will "only report what it would have done," so you see the tarball's files without saving it. The tarball is what lands in node_modules, and it can differ from the repo. A date library has no business shipping a native binary, a minified blob with no source, or a setup.js wired to postinstall, which is the shape of the axios payload. npm audit signatures checks the registry signature of every installed package and "will also verify the provenance attestations of downloaded packages," so it needs the node_modules from the npm ci step. npm's provenance guide shows the output shape: "1267 packages have verified registry signatures", "74 packages have verified attestations". The signature docs say the CLI errors when packages lack signatures on a registry that provides them, which "could mean an attacker might be trying to circumvent signature verification." Provenance tells you which repository and build produced a version, which is what the poisoned axios release lacked. Whether that code is safe to run is still your call. Catch it in the Review Desk before you land I build Hermes IDE, so weigh this section with that in mind. Hermes runs each agent, such as Claude Code or Codex, in its own terminal on its own hermes/ branch and worktree, which is where the commands above run. ⌘G (Ctrl+Shift+G on Windows and Linux) opens the Review Desk. Per the README, it "shows changes by turn or by file with risk flags." In the by-turn view, the package-lock.json change shows up under the turn that made it. !The Hermes Review Desk in By turn view, listing two agent turns with their file and line counts, the diff of turn 1, the changed-files list and a drafted commit message The Review Desk (⌘G) in By turn view for a Claude Code task in orders-api: two turns (T1: 3 files +69 −1, T2: 1 file +1 −0), turn 1's diff of src/db.ts, the changed files and a commit message drafted from the turns. Every agent turn is saved, so you can diff or restore that single turn, with undo. Or leave a line comment on the package.json line, which goes back to the agent that wrote it: "use the built-in Date and Intl APIs, no new dependency." Your own checks can also decide when a task is done, and the worktree recipes doc covers setup per repository. npm test and npm audit signatures are good candidates for those checks. Land then commits, opens a pull request or merges locally into the branch the task started from, and can be undone. Hermes is free, source-available under BSL 1.1, on macOS, Windows and Linux. The current release is 2.3.4, from October 15, 2026. It isn't affiliated with Anthropic, OpenAI, Nous Research or any agent vendor. Download it or read the code on GitHub. Stop the next one earlier All of this happens after the agent ran npm install. On npm 11, dependency install scripts run by default, so a hostile postinstall has already executed by the time you review. Two lines in a committed .npmrc narrow that window: ignore-scripts=true min-release-age=7 min-release-age arrived in npm 11.10.0 on February 11, 2026, and installs only versions "available more than the given number of days ago." Seven days would have kept both poisoned axios versions and plain-crypto-js@4.2.1 out of a fresh install. ignore-scripts will break the odd package that compiles native code at install; handle those one at a time. npm 12, released July 8, 2026, blocks dependency lifecycle scripts by default "unless allowed by the root package's allowScripts policy." That puts the decision in package.json, where the git diff check sees it. While you're there, add one line to your AGENTS.md: ask before adding a dependency. What does your team do when an agent adds a package nobody asked for? Tell me in the comments. --- If this was useful Every check above asks one question about an agent's action: what did it change, and can you take it back? AI That Acts asks it from the builder's side: tool calling and functions in TypeScript, up to a first working agent that acts within limits you set. If you have watched an agent run npm install and wondered how that tool call is wired, start there. ![AI That Acts — AI in TypeScript](amazon.com) It is book 3 of AI in TypeScript, five books from your first LLM call to agents in production: the whole series. And if you review agent work every day, Hermes IDE shows each agent's changes turn by turn before you land them.

    1 reaction
Trending awards, every completed day and week
Gold
21st place finishes
Silver
02nd place finishes
Bronze
13rd place finishes
Awarded repositories
1Owned by npm now

Repositories

Ordered by gold, then silver, then bronze.

  1. npm-expansionsBest: 1st by PR authors, 28 PR authors201

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.