# GitHub Said 55 Vulnerabilities. Here Is How Dependabot Works, How to Set It Up, and How to Tell Which Alerts Matter

How Dependabot finds vulnerable dependencies, how to configure dependabot.yml, how to cut the noise, and the triage method we used on 55 real alerts.

Author: J.A. Watte
Published: October 30, 2026
Source: https://jwatte.com/blog/dependabot-how-it-works/

---

On October 30, 2026 we pushed a one-line fix to a private repository, and GitHub answered with this:

> GitHub found 55 vulnerabilities on [our repository's] default branch (18 high, 23 moderate, 14 low).

That message comes from **Dependabot**, GitHub's built-in dependency scanner. Fifty-five sounds alarming. After an hour of checking, the honest answer was: every alert was technically correct, almost none was a real threat to how we use that code, and one tool we never use accounted for 22 of them.

This post explains how Dependabot works, how to turn it on and configure it, how to keep it from burying you in pull requests, and the triage method we used to sort the 55 alerts.

## What Dependabot is

Dependabot is three related features, plus a few newer extras. The three core features cost nothing on GitHub.com, for public and private repositories alike.

| Feature | What it does | Needs `dependabot.yml`? |
|---|---|---|
| **Dependabot alerts** | Flags dependencies with a known vulnerability | No |
| **Dependabot security updates** | Opens a pull request that moves a vulnerable dependency to a patched version | No (the file can customize it) |
| **Dependabot version updates** | Opens pull requests to keep dependencies current, vulnerable or not | Yes |
| Auto-triage rules | Dismisses, reopens or routes alerts automatically | No |
| Malware alerts | Flags packages listed as malicious | No |

The first two are about security. The third is about staying current, and it is the one that floods a repository with pull requests if you leave it unconfigured.

## How alerts are made

[GitHub's documentation](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/about-dependabot-alerts) describes a simple pipeline:

1. **The dependency graph.** GitHub reads the manifest and lock files committed to your repository's **default branch** (`package.json` and `package-lock.json`, `requirements.txt`, `go.mod`, `Cargo.lock` and so on) and builds a list of every package and version.
2. **The GitHub Advisory Database.** Each advisory names a package and a vulnerable version range. "Only advisories reviewed by GitHub trigger alerts."
3. **The match.** When a version in your graph falls inside a vulnerable range, you get an alert. Matching runs again whenever a new advisory is published or a push changes your dependencies.

Four consequences follow, and the first one surprised us:

- **Dependabot sees what is committed, not what is deployed.** If your build generates or updates a lockfile, the alerts describe the old file in Git, not the software that runs.
- **It does not check whether the vulnerable code is ever called.** A flaw in a library's WebSocket client raises the same alert whether or not you open a WebSocket. GitHub has a separate **reachability analysis** for this, in public preview for npm, pnpm, Yarn and TypeScript projects.
- **GitHub Actions pinned to a commit SHA get no alerts.** Alerts cover actions referenced by a version tag. Version updates can still bump SHA pins.
- **Turning it on does not email you about old problems.** GitHub notifies you only about vulnerabilities found after you enable alerts. The existing ones are on the Security tab.

## Turn it on

In the repository, open **Settings**, then the security settings page (**Advanced Security** on current GitHub; older guides call it "Code security and analysis"). Enable, in this order:

1. **Dependency graph** (on by default for public repositories).
2. **Dependabot alerts.**
3. **Dependabot security updates**, if you want fix pull requests opened for you.
4. **Grouped security updates**, so a fix for one package arrives as one pull request instead of several.
5. **Dependabot version updates.** This opens a template `.github/dependabot.yml` for you to fill in and commit.

Organization owners can apply the same settings to every repository at once from the organization's settings.

## Configure version updates with dependabot.yml

Version updates do nothing until `.github/dependabot.yml` tells Dependabot which package managers to watch, where, and how often. This is a reasonable starting file for a website repository with npm packages, GitHub Actions workflows and Docker images:

```yaml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "06:00"
      timezone: "UTC"
    open-pull-requests-limit: 5
    cooldown:
      default-days: 5
      semver-major-days: 14
    groups:
      npm-minor-and-patch:
        update-types: ["minor", "patch"]
      npm-security:
        applies-to: security-updates
        patterns: ["*"]
    ignore:
      - dependency-name: "sharp"
        update-types: ["version-update:semver-major"]
    labels: ["dependencies"]
    commit-message:
      prefix: "deps"
      prefix-development: "deps-dev"
      include: "scope"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      actions:
        patterns: ["*"]

  - package-ecosystem: "docker"
    directories: ["/services/*"]
    schedule:
      interval: "weekly"
```

What each part does, from the [options reference](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference):

| Key | What it controls |
|---|---|
| `version: 2` | Required; always 2 |
| `package-ecosystem` | The package manager: `npm` (also pnpm and Yarn), `pip`, `uv`, `bundler`, `cargo`, `gomod`, `maven`, `gradle`, `nuget`, `composer`, `docker`, `docker-compose`, `github-actions`, `terraform`, `helm`, `pre-commit` and more |
| `directory` / `directories` | Where the manifest lives. `directories` takes a list and supports `*` globs |
| `schedule` | `interval` is `daily`, `weekly`, `monthly`, `quarterly`, `semiannually`, `yearly` or `cron` (with a `cronjob` line). `day`, `time` and `timezone` refine it; time is UTC unless you set `timezone` |
| `open-pull-requests-limit` | Most open version-update pull requests at once. Default 5; `0` turns version updates off. Security updates have their own limit |
| `groups` | Combine updates into fewer pull requests by `patterns`, `exclude-patterns`, `dependency-type`, `update-types`. `applies-to: security-updates` makes a group for security fixes |
| `cooldown` | Wait a number of days after a release before proposing it. Separate values for major, minor and patch. Security updates ignore it |
| `ignore` / `allow` | Skip or limit dependencies by name, version or update type. `ignore` wins |
| `labels`, `assignees`, `milestone` | Pull request housekeeping. `labels: []` removes the default labels |
| `commit-message` | Commit and pull request title prefix, separately for development dependencies |
| `target-branch` | Branch for version updates. Security updates always target the default branch |
| `versioning-strategy` | How npm, pip, uv, cargo, composer and others edit version ranges: `auto`, `increase`, `increase-if-necessary`, `lockfile-only`, `widen` |
| `exclude-paths` | Glob patterns Dependabot should not scan |
| `registries` | Private registries, with credentials taken from Dependabot secrets such as `${{secrets.NPM_TOKEN}}` |

One option is gone: **`reviewers` was removed in 2025.** GitHub now [assigns reviewers from your CODEOWNERS file](https://github.blog/changelog/2025-08-08-dependabot-reviewers-configuration-option-is-replaced-by-code-owners).

## Keep the noise down

Unconfigured version updates open one pull request per package per release. These settings matter most:

- **Group minor and patch updates.** One weekly pull request per ecosystem is far easier to review than twenty.
- **Set a cooldown.** A few days lets the rest of the world find a broken release before you install it, and it blunts the "malicious new version" style of supply-chain attack. Security fixes still arrive at once.
- **Use a slower schedule for low-risk ecosystems.** GitHub Actions monthly is plenty for most sites.
- **Ignore majors you have decided to hold**, and write down why in the commit.
- **Expect pauses on quiet repositories.** Dependabot pauses version updates when nobody interacts with its pull requests for a long time.

Each pull request shows a **compatibility score**, an estimate of how often the same update passed CI in other public repositories. It is a useful hint, not a guarantee.

## Security updates and transitive dependencies

Most alerts are not on packages you installed yourself. They are on a dependency of a dependency, which npm calls a transitive dependency. In our case all 55 were.

[GitHub's documentation](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates) says what Dependabot can do about them:

- **npm:** it updates whatever you declared, which may mean updating the parent package or removing a sub-dependency the parent no longer needs.
- **NuGet with `PackageReference`:** it tries the parent; if that fails, it pins the transitive package directly.
- **Most other ecosystems:** it cannot fix a transitive dependency when that would require updating the parent.

When no fix is possible, the alert shows an error instead of a pull request. You then have four choices:

1. **Update the parent** yourself, if a newer parent pulls in the fixed version.
2. **Force the version.** npm's `overrides` (Yarn's `resolutions`, pnpm's `pnpm.overrides`) pin a transitive package. A range key limits the override to the vulnerable versions:

   ```json
   "overrides": {
     "undici@>=6.0.0 <6.29.1": "6.29.1"
   }
   ```

   Run your tests afterwards; you are now shipping a combination the parent's authors did not test.
3. **Remove the parent** if you do not use it.
4. **Dismiss the alert** with a reason, if the risk does not apply to you.

## Auto-triage rules

[Auto-triage rules](https://docs.github.com/en/code-security/dependabot/dependabot-auto-triage-rules/about-dependabot-auto-triage-rules) act on alerts before anyone is notified:

- **GitHub presets**, available everywhere. One dismisses low-impact npm alerts in development-only dependencies; it is on by default for public repositories and opt-in for private ones. Another dismisses malware alerts that flag every version of a package, which usually means an internal package shares a name with a malicious public one. It is off by default, for good reason.
- **Custom rules** dismiss, reopen or open pull requests for alerts by severity, package name, CWE and other metadata. They are available on public repositories and, for private organization repositories, with GitHub Code Security.

Auto-dismissed alerts are not deleted. They close with the reason `auto-dismiss`, stay searchable, and reopen if the metadata changes, for example when a development dependency becomes a production one.

## Automate it with GitHub Actions

Workflows triggered by Dependabot pull requests run with tighter rules:

- **Secrets:** they read **Dependabot secrets**, a separate store from Actions secrets. A deploy-preview token stored only as an Actions secret is invisible to them.
- **Token:** set the `permissions` your job needs explicitly.

The common pattern is to merge patch updates automatically once CI passes. This uses GitHub's [fetch-metadata action](https://github.com/dependabot/fetch-metadata) (v3.1.0 as of this writing; pin it to a commit SHA in production):

```yaml
name: Dependabot auto-merge
on: pull_request

permissions:
  contents: write
  pull-requests: write

jobs:
  automerge:
    runs-on: ubuntu-latest
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    steps:
      - id: meta
        uses: dependabot/fetch-metadata@v3
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - if: steps.meta.outputs.update-type == 'version-update:semver-patch'
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Two settings come first. Turn on **Allow auto-merge** in the repository's general settings, or `--auto` fails. Then turn on **Require status checks to pass before merging** for the branch: `--auto` waits only for required checks, so without any the pull request merges at once. If the branch uses a merge queue, `GITHUB_TOKEN` cannot add to it; use a GitHub App token.

To stop new vulnerable packages at the door, add GitHub's [dependency-review-action](https://github.com/actions/dependency-review-action) (v5) to pull request checks. It fails a pull request that adds a dependency with a known advisory, before the alert ever exists. It works on public repositories; private ones need GitHub Code Security.

## How we triaged 55 alerts

The alerts were on the private repository that builds our self-hosted workstation desktop, a container image that bundles command-line tools: AI coding assistants, the Netlify, Cloudflare and Vercel CLIs, MCP servers, Puppeteer and Lighthouse. This is the method, and it works on any repository.

**1. Get the list in a form you can sort.** The Security tab is fine for ten alerts. For fifty, use the API:

```bash
gh api --paginate "repos/OWNER/REPO/dependabot/alerts?state=open&per_page=100" \
  -q '.[] | [.number, .security_advisory.severity, .dependency.package.name,
            .dependency.manifest_path, .security_vulnerability.vulnerable_version_range,
            (.security_vulnerability.first_patched_version.identifier // "none")] | @tsv'
```

All 55 came from two lockfiles: 49 in the desktop's main tool list and 6 in a separate browser-automation bundle. Every one was a transitive dependency. Twenty-eight were the same HTTP client, undici, installed in five different versions by different tools.

**2. Check that the lockfile is what actually ships.** This was the surprise. The lockfile in Git was last changed on October 8. Our desktop updater bumps tool versions when it builds a release, so the desktop running since October 21 had a different lockfile that existed only inside the image. Dependabot was grading an old file. We copied the real lockfile out of the running container and matched every alert against it. All 55 still applied: the newer tools bundled the same vulnerable packages.

**3. Find who pulls each package in.** `npm explain <package>` shows the chain in a project you have installed. For a lockfile alone, a short script that walks `package-lock.json` does the same. The answers were specific: undici came from the Vercel CLI and from Wrangler's local Workers runtime, body-parser and toml from the Netlify CLI, node-forge from Netlify's image tooling.

**4. Read the advisory for its precondition.** Severity is only the score. The text says what an attacker needs:

| Package | Alerts | What an attacker needs |
|---|---|---|
| undici | 28 (10 high) | You connect to a server they control, or your code passes its own certificate-check callback (the one that gets silently dropped) |
| body-parser, toml, fastify, qs, busboy, esbuild | 10 | Your local development server is running and they can send it requests |
| node-forge | 1 high | You verify their signatures with a low-exponent RSA key |
| zod | 2 high | You validate a huge array they supply |
| fast-uri, ip-address, deepmerge | 14 | Minor parsing issues |

For command-line tools on a single-user desktop that nothing on the internet can connect to, none of these is a realistic path in. That is a judgment about our use, not about the packages; the same body-parser alert on a public API server would be urgent.

**5. Simulate the fix before doing it.** We copied the tool list, set every tool to its latest version, and resolved a fresh lockfile without installing anything:

```bash
npm install --package-lock-only --ignore-scripts
```

Matching the 55 alerts against that file gave the plan:

| Action | Alerts cleared |
|---|---|
| Rebuild with current tool versions | 31 |
| Drop the Vercel CLI, which we never signed in to | 22 more |
| deepmerge: no patched version exists; low impact | 2 to dismiss |

**6. Close the loop in Git.** The lasting fix is to commit the lockfile the build actually uses, so Dependabot reports on what ships. Dismiss what remains with an honest reason; the API accepts `fix_started`, `inaccurate`, `no_bandwidth`, `not_used` and `tolerable_risk`:

```bash
gh api -X PATCH repos/OWNER/REPO/dependabot/alerts/55 \
  -f state=dismissed -f dismissed_reason=tolerable_risk \
  -f dismissed_comment="Low impact, no patched version; dev tool on a single-user desktop"
```

## A short checklist

- Turn on the dependency graph, alerts, security updates and grouped security updates on every repository.
- Add `dependabot.yml` with grouped minor and patch updates, a cooldown, and a slower schedule for GitHub Actions.
- Put reviewers in CODEOWNERS, not `dependabot.yml`.
- Store any token a Dependabot pull request needs as a **Dependabot secret**.
- Allow auto-merge and require status checks before you let anything merge itself.
- Commit the lockfile your build really uses, or the alerts describe something else.
- For each alert: is it the shipped version, who pulls it in, what does the attacker need, and does a parent update, an override or removing the tool fix it?
- Dismiss with a reason, and write the reason so the next person can follow it.

Dependabot is very good at the first half of the job: knowing that a vulnerable version is present. The second half, deciding whether it matters, is still yours.

Dependabot also only sees your repository. To check what your live site actually serves to every visitor, such as outdated libraries, public source maps or exposed lockfiles, run the free [Mega Security Analyzer](/tools/mega-security-analyzer/) on it.


---

Canonical HTML: https://jwatte.com/blog/dependabot-how-it-works/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/dependabot-how-it-works.webp
