diff --git a/.github/workflows/deps-commits.yml b/.github/workflows/deps-commits.yml
new file mode 100644
index 0000000000000..7a337419eea3b
--- /dev/null
+++ b/.github/workflows/deps-commits.yml
@@ -0,0 +1,42 @@
+# NOTE: This file is NOT managed by @npmcli/template-oss and may be edited.
+# It enforces that production dependency changes are isolated into dedicated
+# `deps:` commits so they are visible in the git history and changelog.
+
+name: Deps Commits
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - edited
+ - synchronize
+
+permissions:
+ contents: read
+
+jobs:
+ deps-commits:
+ name: Check Deps Commits
+ # Skip bot-generated release PRs, which intentionally bump many
+ # inter-workspace dependency versions inside a single `chore: release` commit.
+ if: ${{ !startsWith(github.head_ref, 'release/') }}
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 26.x
+ check-latest: contains('26.x', '.x')
+ - name: Check Deps Commits
+ run: >-
+ node scripts/check-deps-commits.js
+ --from "origin/${{ github.base_ref }}"
+ --to "${{ github.event.pull_request.head.sha }}"
diff --git a/scripts/check-deps-commits.js b/scripts/check-deps-commits.js
new file mode 100644
index 0000000000000..f3b8cb967cf05
--- /dev/null
+++ b/scripts/check-deps-commits.js
@@ -0,0 +1,164 @@
+// Ensures that any production dependency change in a first-party manifest
+// (root package.json, workspaces/*, mock-registry, etc.) is accompanied by a
+// dedicated `deps:` commit that names the specific dependency. This prevents
+// production dependency bumps from being buried inside unrelated `fix:`/`feat:`
+// commits, which hides the change from the release/changelog tooling.
+//
+// This intentionally uses only Node builtins + git so it can run in CI without
+// a full dependency install (no resetdeps required).
+
+const { execFileSync } = require('node:child_process')
+
+// Only production dependencies are enforced. devDependencies and other fields
+// change frequently for tooling reasons and do not require a `deps:` commit.
+const DEP_TYPES = ['dependencies']
+
+// A commit is a "deps" commit if its subject uses the conventional
+// `deps:` / `deps(scope):` / `deps!:` type prefix.
+const DEPS_SUBJECT = /^deps(\([^)]*\))?!?:/
+
+const git = (...args) => execFileSync('git', args, { encoding: 'utf8' })
+
+const arg = (name, fallback) => {
+ const idx = process.argv.indexOf(name)
+ if (idx !== -1 && process.argv[idx + 1]) {
+ return process.argv[idx + 1]
+ }
+ const inline = process.argv.find((a) => a.startsWith(`${name}=`))
+ return inline ? inline.slice(name.length + 1) : fallback
+}
+
+// Read and parse a package.json at a given git ref. Returns null if the file
+// did not exist at that ref (e.g. a newly added manifest).
+const readJSONAtRef = (ref, file) => {
+ try {
+ return JSON.parse(git('show', `${ref}:${file}`))
+ } catch {
+ return null
+ }
+}
+
+// A first-party manifest is any package.json that is NOT inside a node_modules
+// directory. Bundled dependency manifests under node_modules churn as a
+// side-effect of lockfile/bundle regeneration and must be ignored.
+const isFirstPartyManifest = (file) =>
+ (file === 'package.json' || file.endsWith('/package.json')) &&
+ !file.startsWith('node_modules/') &&
+ !file.includes('/node_modules/')
+
+// Does a commit message mention this dependency by name? Historical formats:
+// deps: undici@6.27.0
+// deps(arborist): validate-npm-package-name ^8.0.0
+// deps!: bump sigstore from 2.x to 3.0.0
+// deps: remove read-package-json-fast
+// The name appears as a standalone token, optionally followed by `@version`.
+const mentionsDep = (message, dep) => {
+ const tokens = message
+ .split(/\s+/)
+ .map((t) => t.replace(/^[`'"([,]+/, '').replace(/[`'").,;:]+$/, ''))
+ return tokens.some((t) => t === dep || t.startsWith(`${dep}@`))
+}
+
+const main = () => {
+ const from = arg('--from')
+ const to = arg('--to', 'HEAD')
+
+ if (!from) {
+ throw new Error('Usage: node scripts/check-deps-commits.js --from