Automatically triage Dependabot alerts with an AI agent
Your Dependabot alert queue does not need you, not at the beginning anyway. It needs a triage process applied consistently: is this vulnerability actually exploited in the wild, is the vulnerable code path reachable in this repo, and does anything else raise the risk to this asset? Then fix or dismiss. Historically, alert triage has been a tedious time-sink focused on updating dependencies guided by CVE severity that is usually irrelevant to your software. The problem is that you have to show up every week, read every advisory, estimate the risk, and craft a fix or defend dismissal for each alert.
This guide shows you how to build an agent that shows up and does the tedious bits for you on a schedule:
- It fetches all of a repo's open Dependabot alerts. No severity filter, because severity is close to uncorrelated with real risk.
- It scores every alert with the k9 Security risk scoring rubric: known exploitation (CISA/VulnCheck KEV), exploit probability (EPSS), code-path reachability, and asset impact, producing a FIX_TODAY / SCHEDULE / DEFER verdict with an evidence line for each finding.
- It prepares fixes for the verdicts that deserve them: a dependency bump on a branch, the repo's test suite run, and a pull request opened only when tests pass.
- It writes a full triage report to Confluence as a durable audit trail, and files a concise Jira summary that links to the report and to the agent's own session transcript.
- It pulls you in only when there is something to decide. A run with findings lands in your review queue as an In Progress issue with ready-to-run dismissal commands. A clean run closes its own issue as Done.
This agent doesn't dismiss alerts or merge PRs (though it could). It does the reading, the risk scoring, the evidence-writing, and the fix preparation. You keep making the decisions, but with an evidence-based recommendation for each alert.
Here is a condensed slice of a real run's Jira summary, produced unattended by the k9 pipeline this guide describes:
Alert verdict counts: 0 FIX_TODAY / 0 SCHEDULE / 63 DEFER. Dedupe: 63 open alerts → 49 unique vulnerabilities. Noise removed: 100%. fastmcp (4 CVEs, including the one critical, CVE-2026-32871 OpenAPI SSRF), DEFER. Used only as a SigV4 AWS-MCP client; the vulnerable OpenAPI and OAuth-proxy server features are never instantiated (unreachable). pyjwt (6 CVEs), DEFER. The agent verifies no inbound JWTs and never runs PyJWKClient (unreachable). Resources: Full report: Dependency Alert Triage Report: demo-k9-cloudsec-agent - 2026-07-09 · Claude Agent Session
The critical-severity finding is the point of the whole system. Its CVSS label said act now. The reachability evidence showed the vulnerable server features are never instantiated in this codebase, and the verdict says so, with the citation attached. And the summary above is the entire week of vulnerability management for that repo: the agent read and scored all 63 alerts, and your remaining work is a thirty-second review of one Jira issue, with the full evidence one click away.
A note for AI-agent readers: this document is written to be executable. The routine prompt in the appendix is the complete behavioral specification. Every configuration value that must be exact is given exactly. Where a value is yours to choose, it appears as an <ANGLE_BRACKET_PLACEHOLDER>.
Architecture
Five components, each doing one job:
| Component | Role |
|---|---|
Claude Code cloud routine (https://code.claude.com/docs/en/routines) |
The scheduler and runtime. A cron-triggered, sandboxed cloud session with a fresh clone of your repo. One routine per repo. |
k9 Security MCP server (https://mcp.k9security.io/mcp) |
The security brain: the versioned risk-scoring rubric, the triage workflow procedures, and the score_risk / lookup_kev / lookup_epss tools. |
| GitHub Dependabot | The system of record for alerts, read through GitHub's dedicated Dependabot MCP server (distinct from the standard GitHub MCP server; see Step 1). |
Atlassian MCP (https://mcp.atlassian.com/v1/mcp) |
The output surfaces: a Confluence page per run (full report) and a Jira issue per run (summary + links). |
| A dedicated cloud environment | Holds the GitHub token, the network allowlist, and nothing else. |
The weekly flow:
cron fires (Tuesday morning)
└─ cloud session starts: fresh repo clone, MCP servers connect
├─ read the triage procedures + rubric LIVE from the k9 MCP server
├─ fetch ALL open Dependabot alerts (GitHub Dependabot MCP)
├─ score every unique finding (k9 score_risk, KEV + EPSS + reachability + impact)
├─ prepare warranted fixes: branch → bump → tests → PR (only if tests pass)
├─ publish the full triage report to Confluence
├─ file the Jira summary with Resources links (report + session transcript)
└─ transition the issue: clean run → Done, findings → In Progress
Three design decisions that matter more than the setup
The procedure lives on the server, not in the prompt. The routine prompt tells the agent to read the triage workflow and rubric from the k9 MCP server at the start of every run, and forbids working from memory. The k9 server publishes these as versioned MCP prompts and resources (fetch_dependency_alerts, score_dependency_alerts, fix_dependency_alerts, summarize_dependency_alerts, and the risk_scoring_rubric). When k9 revises the rubric or the report format, every agent, scheduled or interactive, picks up the new version on its next run with zero configuration changes. A prompt that restates a procedure is a cache with no invalidation.
Handle errors in the routine prompt. Unattended agents fail in unattended ways, and a failed run is only useful if it leaves the operator enough to debug and resolve it. So give every step that touches an external system explicit failure handling. In our prompt, a failed alert fetch reports the token's presence (never its value) and details of the HTTP response, then files the Jira report and stops. Failed tests block the PR, and their output goes in the report. Failures the prompt does not enumerate inherit the pattern: put the exact evidence in the Jira report a human will read. Our first four runs all failed, and each blocked-run report carried precisely the evidence that identified the next fix.
Human-in-the-loop by exception, with everything pre-staged. The agent's write actions stop at PR creation. Alert dismissals ship as ready-to-run gh commands with rubric-cited comments. So accepting a DEFER verdict costs one copy-paste in your terminal or one instruction to an agent. Then the closure is defensible in any later audit. Clean runs close their own Jira issue. A run only enters your review queue when it found something, opened a PR, or failed.
Prerequisites
- Claude Pro, Max, Team, or Enterprise, with Claude Code cloud sessions (claude.ai/code) available on your plan. Costs: there is no separate compute charge for the cloud VM and no additional direct cost to run this solution on your plan. Runs do consume your session and weekly rate limits, which Claude Code on the web shares with all other Claude and Claude Code usage in your account (plan details).
- A k9 Security account with the k9 MCP server connected at claude.ai (Settings → Connectors). Reachable Risk Personal is $10/month with a 7-day trial; setup starts at k9security.io/lp/reachable-risk. k9 MCP config
- GitHub organization with Dependabot alerts enabled on the target repos, and the Claude GitHub App installed for the organization (required for cloud sessions to clone and open PRs).
- Atlassian Jira project + Confluence space, with the Atlassian MCP connector connected at claude.ai.
- Repo bootstrapping and tools the agent depends on: documented dependency-install and code test commands (Makefile targets, shell scripts, or CLAUDE.md instructions). Remember the cloud session is a fresh clone: the agent must be told how to install dependencies before it can run your tests.
Step 1: Create a fine-grained GitHub token for alert reads
The Dependabot alerts API requires a permission the Claude GitHub App does not have, so the agent needs its own credential for this one job.
Create a fine-grained personal access token (GitHub → Settings → Developer settings → Fine-grained tokens):
- Resource owner: your organization
- Repository access: only the repos your routines will triage
- Repository permissions: Dependabot alerts → Read-only (Metadata is added automatically). Nothing else.
- Expiration: your call; note the date. When it lapses, every weekly run files a blocked-run report naming the 401, so the failure is self-diagnosing, but a calendar reminder is cheaper.
If your organization requires approval of fine-grained tokens, approve the token in the org settings before testing.
Why not the Claude GitHub App, GitHub MCP, a REST call, or OAuth? We tried every intuitive path and each is walled off. Documenting the walls saves you the hours they cost us and justifies the PAT:
- The Claude GitHub App cannot read Dependabot alerts. Its permission set does not include them, and installers cannot grant permissions an app does not request.
- The default GitHub MCP toolset has no Dependabot tools, in both the sandbox's built-in server and the default remote endpoint.
- Direct REST calls to
api.github.comfrom the cloud sandbox are futile. A dedicated GitHub proxy transparently replaces your Authorization header with the Claude App's token. The failure is deceptive:GET /userreturns 200 as you (the app token acts on your behalf), while the alerts endpoint returns 403 "Resource not accessible by integration". Integration means app token. Your token never reaches GitHub on that host, no matter what you put in the header. - The Dependabot-scoped MCP endpoint does not support OAuth Dynamic Client Registration, so it cannot be added as a claude.ai connector. A PAT in a request header is the supported authentication.
The working path is GitHub's dedicated Dependabot MCP server at api.githubcopilot.com/mcp/x/dependabot, authenticated with the fine-grained PAT and declared in the repo. This endpoint is distinct from the standard GitHub MCP server people connect to their Claude accounts, and the distinction is why the whole detour exists: the standard server has no Dependabot alert tools. That is Step 3.
Step 2: Create a dedicated cloud environment
Cloud sessions run inside an environment that controls network access and environment variables. Make a dedicated one so the token and allowlist have the smallest possible blast radius (ours is named k9-appeng).
From claude.ai/code, open the environment selector (the cloud icon anywhere you start a session or edit a routine) → Add environment:
- Name: something purpose-scoped, like
appeng-automation - Network access: Custom, with one allowed domain:
api.githubcopilot.comand check "Also include default list of common package managers." The defaults cover github.com, api.github.com, and the package registries your test suite needs. The one custom entry is the host of GitHub's dedicated Dependabot MCP server (Step 3), which is not in the default list. Nothing else is required: MCP connector traffic (k9, Atlassian) routes through Anthropic's servers and bypasses the egress allowlist entirely, and git operations ride the dedicated GitHub proxy. - Environment variables:
GITHUB_DEPENDABOT_TOKEN=github_pat_XXXXXXXXHeads-up: environment variables here are stored in plaintext, visible to anyone who can edit the environment. There is no dedicated secrets store for cloud environments today. That is probably acceptable for a read-only, repo-scoped, expiring token on a personal account. Check with your security team for an organization account and recommended expiry.
Step 3: Declare the Dependabot MCP server in each repo
Add (or extend) .mcp.json at the repo root with the Dependabot MCP server declaration:
{
"mcpServers": {
"github-dependabot": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/x/dependabot",
"headers": {
"Authorization": "Bearer ${GITHUB_DEPENDABOT_TOKEN}"
}
}
}
}
Environment-variable expansion in .mcp.json headers is supported, so no secret enters the repo. Commit and push this to the branch your routine clones (usually main). The cloud session reads the repo's checkout, not your local state.
Local side effect worth knowing: developer sessions without GITHUB_DEPENDABOT_TOKEN set will show this server as failing to connect. That is harmless noise; note it in the repo's CLAUDE.md if your team will ask.
Step 4: Prepare the Atlassian integration points
Next, configure the places where you'll integrate the agent's triage process and reports back into your workflow.
- Create one Confluence folder per repo to hold the weekly reports (ours: a folder per repo in the
Securityspace). Note the space key (the<SPACE>value, e.g.SEC) and capture each folder's numeric id from its URL. - Pick the Jira project and label (
dependency-triage) the agent will use to create issues for tracking the triage workflow. Look up your workflow's transition ids for "In Progress" and "Done" (the agent can also discover them at runtime withgetTransitionsForJiraIssue).
Step 5: Create the routine
From claude.ai/code/routines (or via Claude Code's scheduling support), create one routine per repo:
- Schedule: weekly, stagger and run off-hours if you have several routines. Claude Code on the web shares rate limits with all other Claude and Claude Code usage within your account.
- Environment: the dedicated environment from Step 2.
- Repository: the target repo.
- Model: we run
claude-opus-4-8. Rubric-following with reachability judgment is the hard part of this job; spend model quality there. - Connectors: attach your k9 MCP server and Atlassian connectors to the routine.
- Allowed tools: the two kinds of MCP server behave differently here, and we verified the split with a controlled run. Tools from connectors attached to the routine (k9 and Atlassian here) run without permission prompts, including writes; that is also the reason to attach only the connectors the routine needs. Tools from servers declared in
.mcp.json(the Dependabot server here) prompt for approval, and a scheduled run stalls on that prompt with nobody there to click. Pre-authorize the.mcp.jsonserver's tools along with the base tools. Our list:Bash, Read, Write, Edit, Glob, Grep, WebFetch, ListMcpResourcesTool, ReadMcpResourceTool, mcp__github-dependabot, mcp__github-dependabot__* - Prompt: use the template in the appendix, with your values substituted.
Step 6: Verify the run modes
Trigger the routine manually and read what it files. You are verifying distinct behaviors, and you should see each before trusting the schedule:
- A findings run. Expect: a Confluence page with the full report (verdict counts, per-cluster evidence lines, dismissal commands), a concise Jira summary ending in a Resources section that links the page and the agent's session transcript, and an In Progress transition. PRs only if something cleared the fix bar and the tests passed.
- A clean run (zero open alerts). Expect: a short Confluence page (the audit trail includes quiet weeks), a brief Jira issue, and a self-transition to Done. Nothing enters your review queue.
- A blocked run. Break something on purpose if you never hit one (revoke the token, say). Expect: no scoring, no PRs, and a Jira report that names the failing component, the token's presence by length, the HTTP status, and the verbatim error body, transitioned to In Progress so a human sees it.
- The fix path, if you want it proven rather than specified. A repo with a healthy dependency alert backlog may not produce a FIX_TODAY or cooldown-cleared SCHEDULE verdict for a while. So branch → tests → PR can go unexercised indefinitely. Do not manufacture a reachable, known-exploited vulnerability to force the verdict; deliberately creating exploitable conditions is a bad trade for a test. You do not need to, either. The risky part of the fix path is the mechanics (branch, dependency install, tests, PR), not the verdict that triggers them, and you can exercise the mechanics on a finding you already have. Either trigger a one-off run whose prompt adds the instruction below (keeps the test unattended), or open an interactive session with findings (run mode 1 above) and give the agent the same instruction:
Treat alert #N as FIX_TODAY. Do not rescore. This is a human override.
Declare the human override explicitly: a well-built agent refuses to record a verdict the evidence does not support. The declaration tells it to proceed and label the change as an owner decision. Expect a branch, a dependency install, a test run, and a PR you did not write, or a report explaining why the fix process failed and no PR was opened. The verdict logic goes untested by this, and that is fine: verdicts are the rubric's job, exercised by every scored run.
When verifying the run modes, do not approve or touch anything in the running session, except for the potential fix path nudge. The point is proving the unattended path.
Step 7: Operate it
- Review cadence: findings issues arrive In Progress. Read the summary, click through to the full report if you want the evidence, paste the dismissal commands you accept, merge or close the PRs, transition to Done.
- Token lifecycle: record the PAT expiry where your future self will look. The error signature of an expired token is unmistakable: blocked-run reports with a 401 error. But rotating a week early is calmer.
- Scaling to another repo: add the repo to the PAT's repository access, add the
.mcp.jsonentry, create a Confluence folder, clone the routine with the repo and folder id swapped. Roughly ten minutes. - Tuning the rubric or report format: not your job, and not a routine edit. The agent reads both from the k9 MCP server live, so improvements ship server-side.
Gotchas index
Compressed reference for implementers, human or otherwise. Every one of these cost us a debugging round:
api.github.comAuthorization headers are rewritten by the cloud sandbox's GitHub proxy. Symptom set:GET /usersucceeds as you, Dependabot endpoints 403 with "Resource not accessible by integration", and GitHub token telemetry shows your PAT never used. Use the Dependabot MCP server, never REST, for alert reads.ghCLI is not installed in cloud sessions. Ready-to-runghcommands in reports are for the human's machine..mcp.json-declared MCP servers prompt for tool approval; connector tools do not. A scheduled run stalls on that prompt with nobody to click it. Pre-authorize the.mcp.jsonserver's tools in the routine's allowed tools; connector tools run unprompted, including writes.api.githubcopilot.comis not on the default egress allowlist. Without the custom allow entry, the Dependabot MCP server fails to connect with a CONNECT-tunnel 403 before the token is ever presented.- The Dependabot MCP endpoint (
/mcp/x/dependabot) has no DCR, so it cannot be a claude.ai OAuth connector. PAT header auth via.mcp.jsonis the path. - Jira issue creation mangles multi-line markdown descriptions. Create the issue with a one-line placeholder, then set the real description with an edit call, which renders markdown correctly. Verify the body after.
- The Atlassian MCP markdown parser can choke on tables (a spurious "Emoticon requires an id or short name" error). Prefer bullet lists in Jira descriptions; for Confluence pages, retry in ADF format if markdown fails.
- Confluence page titles are unique per space. Same-day re-runs must update the existing day's page rather than create a duplicate. The prompt template handles this.
- GHSA-to-CVE alias resolution is the agent's job. KEV and EPSS are CVE-keyed; an alert keyed only by GHSA id needs its CVE alias resolved before threat lookups, and a genuinely CVE-less advisory still gets scored rather than skipped.
What this buys you
Our four repos now get severity-blind, evidence-cited triage every Tuesday morning before anyone is at a desk. The recurring cost of vulnerability management dropped from an engineer-session per repo per week to reading one to four short Jira issues, most of which closed themselves. The dismissals that used to be tribal knowledge ("we looked at that one, it's test tooling") are now rubric citations attached to the alert in GitHub, written at the moment of decision.
And the count that matters: across those repos this week, zero findings warranted immediate fixes, and the agent proved it instead of vibing it. That is the point of risk-based triage. Severity said act on everything. The evidence disagreed: nothing exploited, nothing probable, nothing reachable. The agent filed that proof where the auditor will look.
Appendix: the routine prompt template
Substitute <ORG>, <OWNER>, <REPO>, <CONFLUENCE_FOLDER_ID>, <ATLASSIAN_SITE>, <SPACE>, <JIRA_PROJECT>, <DEPS_COMMAND> (e.g. make venv-dev to create a Python virtual environment), <TEST_COMMAND> (e.g. make quick to lint and run unit tests), and the transition ids for your Jira workflow. The k9 MCP connector is named k9 here; use your connector's name if it differs.
You are <ORG>'s weekly dependency-alert triage agent for the <REPO> repository (this
checkout). Run the full triage loop, publish the full report to Confluence, and file a
summary Jira issue. Work autonomously; never wait for user input.
PROCEDURE (the canonical text lives on the connected k9 MCP server):
1. Read these MCP resources and follow them as the authoritative procedure:
k9://workflow/fetch-dependency-alerts, k9://workflow/score-dependency-alerts,
k9://workflow/fix-dependency-alerts, k9://workflow/summarize-dependency-alerts, and
the scoring rubric k9://rubric/risk-scoring. Do not work from a remembered copy.
2. Fetch ALL open Dependabot alerts via the 'github-dependabot' MCP server declared in
this repo's .mcp.json (GitHub's dedicated Dependabot MCP server at
api.githubcopilot.com, authenticated by the GITHUB_DEPENDABOT_TOKEN environment
variable). Use its list_dependabot_alerts tool for <OWNER>/<REPO> with state=open,
paginating until complete. Do NOT fetch via curl or REST against api.github.com:
the sandbox's GitHub proxy rewrites Authorization headers on that host to the
Claude GitHub App token, which lacks the Dependabot-alerts permission; direct REST
always 403s regardless of the token you supply. Never pre-filter by severity
(severity is recorded, never gated on). Zero open alerts is a normal outcome, not
an error. If the github-dependabot MCP server is unavailable or its tools fail:
report in the Jira issue (a) whether the server connected and which tools it
exposed, (b) whether GITHUB_DEPENDABOT_TOKEN was present and non-empty (report its
character length, NEVER its value), and (c) the exact error text, then stop after
filing the Jira report.
3. Score every unique finding with the score_risk tool: max 50 findings per call,
dedupe by finding key. Resolve GHSA/GO/ALAS identifiers to their CVE alias first
(the alert payload usually carries cve_id; otherwise
curl -sS https://api.github.com/advisories/<ghsa-id> is a public endpoint) because
KEV/EPSS evidence is CVE-keyed. A genuinely CVE-less advisory still scores with
Threat unknown; never block triage on a missing CVE.
ACTIONS (scope: report + PRs; NO alert dismissals, NO merges):
4. For FIX_TODAY verdicts, and for SCHEDULE verdicts with a fixed release at least
7 days old (cooldown rule), prepare the fix. If an open Dependabot PR already
covers it and is current, note it in the report as ready-to-merge but do not merge.
Otherwise: bump the dependency on a branch, install dev dependencies
(<DEPS_COMMAND>), run the repo's test suite per its CLAUDE.md / Makefile
(<TEST_COMMAND>), and open a PR only
if tests pass, one PR per cluster of same-package fixes. If tests fail, do not open
the PR; include the failure output in the report. Respect any dependency
conventions documented in the repo's CLAUDE.md.
5. Do NOT dismiss any alerts. For each DEFER cluster, prepare a ready-to-run dismissal
command (the user runs these locally where gh is installed):
gh api -X PATCH /repos/<OWNER>/<REPO>/dependabot/alerts/<number>
-f state=dismissed -f dismissed_reason=tolerable_risk
-f dismissed_comment='DEFER per k9 rubric v<version>: <evidence line>'
Use dismissed_reason=not_used instead when the rubric basis is unreachable/unused
code. These commands go in the full report (step 6).
FULL REPORT TO CONFLUENCE (EVERY run, including clean 0-alert runs):
6. Produce the full Dependency Alert Triage Report in markdown by following the
k9://workflow/summarize-dependency-alerts workflow. Include the ready-to-run
dismissal commands from step 5. For a clean run (0 open alerts), the report is
short (state the clean result and the run details) but still produce and publish
it: the weekly page series is the durable audit trail.
7. Store the full report as a NEW Confluence page (site <ATLASSIAN_SITE>, space
'<SPACE>'), parented under the <REPO> reports folder, folder id
<CONFLUENCE_FOLDER_ID>. Title: 'Dependency Alert Triage Report: <REPO> -
<YYYY-MM-DD>' (today's date at the end). If a page with that exact title already
exists (same-day re-run), update that page instead of creating a duplicate. Known
gotcha: the Atlassian MCP markdown parser can fail on markdown tables; if page
creation fails with the markdown body, retry using ADF format (or convert tables
to bullet lists as a last resort). Capture the page URL and exact page title for
the Jira issue.
JIRA SUMMARY ISSUE (Atlassian MCP, site <ATLASSIAN_SITE>, project <JIRA_PROJECT>):
8. Create a <JIRA_PROJECT> Task titled 'Weekly dependency triage: <REPO> -
<YYYY-MM-DD>' (today's date, at the end of the title). Known gotcha: issue
creation mangles multi-line markdown descriptions; create the issue with a
one-line placeholder description, then set the real description via an edit call
(markdown renders correctly there), and verify the body afterward. Add label:
dependency-triage. The description is a CONCISE SUMMARY of the full report, not
the full report: verdict counts (N FIX_TODAY / N SCHEDULE / N DEFER) with dedupe
math; one line per cluster (package, CVE, verdict, evidence gist); PRs opened
(links); ready-to-merge Dependabot PRs; anything needing a human decision; note
that ready-to-run dismissal commands are in the full report. For a clean run, the
summary is simply the clean result. Prefer bullet lists over markdown tables. End
the description with a 'Resources' section whose URLs are formatted as markdown
LINKS (never bare URLs), in exactly this shape:
**Resources:**
* Full report: [<exact Confluence page title>](<Confluence page URL>)
* Claude Agent [Session](<session URL>)
where the session URL is constructed from the CLAUDE_CODE_REMOTE_SESSION_ID
environment variable by replacing its 'cse_' prefix with 'session_'
(echo "https://claude.ai/code/${CLAUDE_CODE_REMOTE_SESSION_ID/#cse_/session_}").
9. Transition the issue AFTER the description is set. For a CLEAN run (0 open alerts
fetched successfully, nothing for a human to review): transition to Done
(transition id <DONE_ID>). For every other run (findings reported, PRs opened, or
a blocked/failed run): transition to In Progress (transition id <IN_PROGRESS_ID>)
so the user reviews it. If a transition id fails, discover ids with
getTransitionsForJiraIssue. This step is mandatory for every run; never leave the
issue in the backlog. If any Atlassian call times out, query current state before
retrying: the mutation has usually succeeded; do not create duplicate issues or
pages.