This guide shows how to give one repository a recurring dependency-remediation job. On every run, Claude Code:
- pulls the latest default branch;
- reads current findings from Socket, Snyk or another dependency scanner;
- checks that the reported vulnerable version is actually present in the repository's resolved dependency graph;
- applies one supported package upgrade;
- runs that repository's unit tests, integration tests and build;
- opens or updates a draft pull request; and
- repeats until no fixable Critical, High or Medium package findings remain.
I call that job a Claude Code routine in this article. It is not a separate product or a magic “autofix CVEs” feature. It is three things you control: a written operating contract, repository-specific commands and a recurring Claude Code invocation using local /loop or cloud /schedule.
The routine prepares a reviewable change. It never merges, suppresses a finding or changes branch protection.
Socket is the scanner in our implementation. Nothing after the scanner adapter is Socket-specific: a team using Snyk, Dependabot alerts or an internal service can feed the same normalized finding into the same loop.
What you need in each repository
Before scheduling anything, prepare four inputs:
| Input | What it contains |
|---|---|
| Scanner adapter | A read-only command or API call that returns package, installed version, severity, vulnerable range and fixed version |
| Repository config | Install, dependency-path, unit, integration and build commands |
| Claude Code contract | Scope, fix order, stop conditions and actions the agent must never take |
| Bot identity | Scanner read access plus permission to push remediation branches and open draft PRs—without merge or branch-protection bypass |
Keep the scanner adapter small. Its job is to translate vendor output into one shape; it should not decide how to edit the repository. The complete contract and a sample repository config appear later in this guide.
Run the contract manually before adding a schedule. The first useful milestone is not an opened PR. It is a clean no-op run against a repository with no actionable findings.
Separate package remediation from image remediation
The routine described here fixes dependencies managed by npm, pip, Poetry, Maven, Gradle, Go modules, Cargo and similar application package managers. Container scanners can report those packages alongside operating-system packages, but they require different fixes:
| Finding | Example | Correct fix path |
|---|---|---|
| Application package | axios, django, log4j-core, a vulnerable transitive npm package | Change the manifest or resolved dependency graph, then run application tests |
| OS package | openssl, libexpat or glibc supplied by Alpine, Debian or Ubuntu | Change or rebuild the base image, then scan the exact rebuilt artifact |
Before automating OS fixes, reduce the OS surface:
- Use
scratchwhen the application can ship as a genuinely self-contained static binary. Copy in only the runtime data it actually needs, such as a CA certificate bundle or timezone data. - Use a distroless image when the application needs a runtime or a small set of runtime libraries but does not need a shell or package manager in the final image.
- Use a minimal, maintained distribution image when native modules, shared libraries, debugging requirements or the runtime make
scratchand distroless impractical.
Those choices reduce exposure; they do not eliminate the remediation problem. A normal Node.js, Python or JVM service still needs its runtime and supporting libraries; copying them into scratch effectively makes the team responsible for assembling and tracking its own minimal runtime image. Distroless images intentionally remove shells and package managers, but they still contain runtime components and system libraries that can receive advisories. Neither choice removes vulnerabilities in application dependencies bundled into the image.
Use multi-stage builds to keep compilers and package managers out of the final image. Then scan the final artifact. When an OS fix exists, prefer moving to a maintained patched base-image tag or digest and rebuilding. Do not add an opaque apt upgrade or apk upgrade layer merely to make a scanner count fall; that makes the final package state harder to trace back to the Dockerfile and base image.
This is why we keep two queues. The package routine changes manifests and lockfiles. The image routine changes the base-image lineage and rebuilds the artifact. A single Trivy or Grype report can feed both queues, but it should not produce one mixed pull request.
Package lane
- manifest and lockfile
- direct and transitive packages
- minimum compatible patched version
- unit, integration and build tests
- draft dependency pull request
Image / OS lane
- Dockerfile and base-image digest
- apk, apt, rpm and system libraries
- vendor-supported fixed package
- rebuild and scan exact image
- separate base-image pull request
One repository, one loop
Do not point one Claude session at twenty repositories. Give every repository its own routine, configuration, credentials and test commands. A central scheduler can start twenty jobs, but each job should have one working tree and one definition of done.
This keeps failures local. If a framework upgrade breaks one API in repo-a, it should not stop a routine from preparing an unrelated fix in repo-b. It also makes repository ownership and pull-request attribution obvious.
Every pass through our loop does the following:
- Sync the current default branch and require a clean working tree.
- Fetch a fresh scanner result for that repository.
- Compare every reported package and installed version with the current lockfile or resolved dependency graph.
- Keep only Critical, High and Medium findings with a verified fixed version.
- Group the remaining findings by package and dependency path.
- Change one package, then run the repository's real tests.
- Keep the change if the tests pass; restore it and record the failure if they do not.
- Open or update a draft pull request with the dependency path, diff and test output.
- Scan the proposed state again and repeat until no actionable findings remain.
The incremental part is important. A command that upgrades twelve packages may produce a green build, but a red build tells you almost nothing. One package at a time gives the routine a clean checkpoint and a useful failure reason. It can continue draining a large backlog without carrying a broken change forward.
Define “the right fix” before allowing writes
Changing a lockfile is not automatically a remediation. The right fix removes the vulnerable version from the resolved graph without hiding the finding or bypassing the package manager.
Use this order:
- For a vulnerable transitive package, upgrade its direct parent to the smallest version that permits a fixed child version.
- For a direct dependency, upgrade to the minimum supported patched version. Avoid unrelated version churn in a security PR.
- Use a major upgrade only when the fix requires it. Treat changed methods, removed exports and configuration migrations as code changes, not lockfile maintenance.
- Allow a lockfile-only refresh only when the manifest range already permits the patched version. Regenerate it with the package manager and prove the vulnerable version disappeared from the graph.
- Use overrides or resolutions as a temporary last resort. Require an owner, an expiry or removal ticket, and evidence that the parent works with the forced version.
Never edit a generated lockfile by hand. Never replace a vulnerable transitive version without checking which parent selected it. Never call a PR fixed because the manifest changed while the old package remains elsewhere in the lockfile.
Socket's targeted fix command can update a direct parent when that is the compatible route to a fixed transitive dependency. Run it for one advisory or package at a time:
socket fix --id GHSA-xxxx-xxxx-xxxx --org YOUR_ORG
Socket also supports bulk fixing, but its own guidance warns that --all makes a failed test harder to attribute. That is exactly why the routine applies and tests one package before moving on.
Why Dependabot is not the whole loop
Dependabot is a good default. GitHub builds its dependency graph from manifests and lockfiles, and calls lockfiles the most reliable representation because they contain resolved direct and transitive versions. Dependabot security updates then try to move a vulnerable dependency to a patched version.
The gap appears when “update package B” is not a valid operation. A parent may constrain B to a vulnerable range. GitHub documents that Dependabot can fail when it cannot resolve a non-vulnerable dependency graph. For npm it may update the parent; for some other ecosystems, a transitive fix that also requires a parent update cannot be produced automatically.
There are two other differences in our loop:
- It uses the security team's scanner and ignore policy, then verifies the finding against the current repository before editing.
- It runs the repository's chosen unit, integration and build commands, restores a failed upgrade, records why it failed and continues with the next package.
So this is not an argument for turning Dependabot off. Keep it if it works for your repositories. The Claude Code routine is useful for the backlog that needs dependency-path reasoning, repository-specific validation and repeated reconciliation.
Also, review a lockfile-only PR with the right question. “Did the lockfile change?” is weak. Ask: Does the newly resolved graph exclude every vulnerable version, does a fresh scan agree, and did the application tests pass?
The stale-finding gate we added after running Socket
In our Socket-backed loop, we saw findings associated with package versions from older releases even though the repository had already moved on. The alert was real for the old release; applying another upgrade to the current branch would not have fixed anything.
We now require these fields before a finding becomes actionable:
{
"repositoryCommit": "current-default-branch-sha",
"package": "example-package",
"reportedInstalled": "2.3.1",
"resolvedInstalled": "2.3.1",
"severity": "high",
"identifiers": ["GHSA-xxxx-xxxx-xxxx"],
"vulnerableRange": "<2.4.0",
"fixedVersion": "2.4.0",
"dependencyPath": ["app", "parent-package", "example-package"],
"packageType": "application"
}
The routine rejects the finding from the fix queue when:
- the scan commit does not match the current default branch;
reportedInstalleddoes not match a version in the current resolved graph;- the resolved version is outside the advisory's vulnerable range;
- the scanner provides no fixed version;
- the record is an OS package and therefore belongs to the image lane.
Reject does not mean delete. The run reports it as stale, not applicable to the current graph, no-fix or out of scope. That preserves the evidence without creating churn.
Method changes are a test problem, not a prompt problem
Suppose a patched major version removes a method your code calls. No prompt can guarantee compatibility by reading the version number. The routine has to install the candidate, compile or build the application, and execute tests that cover the integration.
For each repository, declare exact commands rather than asking Claude Code to guess:
repository: acme/payments-api
default_branch: main
scanner: socket
severity: [critical, high, medium]
commands:
install: pnpm install --frozen-lockfile
resolve: pnpm why "${PACKAGE}"
unit: pnpm test
integration: pnpm test:integration
build: pnpm build
pull_requests:
draft: true
merge: never
Use the equivalent commands for pip, Poetry, Maven, Gradle, Go or Cargo. Commit this file beside the routine—without credentials—so a test rename is reviewed like any other repository change.
If the repository has no tests around the affected integration, the loop has not proved the upgrade safe. For a patch release, you may decide that build plus smoke tests meet your policy. For a major upgrade or a changed public method, stop and ask for a test or human review. Do not turn missing coverage into a green check by omitting the command.
Scan container images locally, but keep a separate fix path
Trivy and Grype can scan the exact image built from the proposed branch. This is useful even if a registry scanner will run later: the agent gets feedback before opening the PR.
GIT_SHA="$(git rev-parse --short HEAD)"
IMAGE="payments-api:cve-${GIT_SHA}"
docker build --pull --tag "${IMAGE}" .
trivy image \
--ignore-unfixed \
--severity MEDIUM,HIGH,CRITICAL \
--exit-code 1 \
--format json \
--output trivy.json \
"${IMAGE}"
grype "docker:${IMAGE}" \
--only-fixed \
--fail-on medium \
--output json \
--file grype.json
Trivy's --ignore-unfixed and Grype's --only-fixed make the fix queue match the same rule as the package loop: work on Critical, High and Medium findings only when the data source names a fix. Keep the complete scan elsewhere if your reporting program needs no-fix visibility; do not pretend that filtering the remediation queue removed those risks.
Parse the result by target and package type:
- Language packages found inside the image go back to the manifest-and-lockfile routine.
- Alpine, Debian, Ubuntu and other OS packages go to the image routine.
- For OS findings, prefer a maintained, newer base-image tag or pinned digest and rebuild. Do not add a one-off
apt upgradelayer merely to make the report green. - If the distribution has not published a fixed package, record
no vendor fix. If the base OS is end-of-life, upgrading the distribution is the fix; hiding unfixed output is not.
Run the scanner against the image you just built, and record the image reference or digest in the PR. Scanning latest later can test a different artifact.
Local loop or cloud schedule?
Claude Code supports both. The choice is mostly about where the repository's dependencies and credentials are reachable.
Use local /loop when | Use cloud /schedule when |
|---|---|
| Private registries, VPN services, local Docker or test databases are already available on the machine | The job must continue while laptops are asleep and the cloud session can reach every required service |
| You are still tuning commands, permissions and failure handling | The routine already produces predictable no-op, success and partial-failure runs |
| You want to watch the first remediations closely | You want a durable cadence across many independently configured repositories |
Anthropic currently documents /loop as a local recurring task that can run for up to three days. Cloud /schedule jobs continue when the laptop is closed, but each clean cloud session must recreate the environment: dependencies, scanner CLI, GitHub authentication, private registry access and repository instructions.
Start locally. Move a repository to cloud only after its setup script can reproduce the same install, scan and test environment without relying on state from a developer laptop. Some repositories may stay local because their private network or integration environment cannot be exposed to the cloud runner. That is a valid architecture, not a failed migration.
Pull-request shape: independent, consolidated or stacked
The loop fixes one package at a time, but that does not force one PR per CVE. Choose the shape from the actual dependency relationship:
One coherent change
Several findings disappear through the same package upgrade and test surface.
Unrelated fixes
Each upgrade can merge, fail or be reverted without blocking the others.
Real dependency
A runtime or framework upgrade must land before the application changes above it.
GitHub now has native stacked pull requests. They fit a remediation such as runtime upgrade → framework upgrade → application API changes. Each PR targets the branch below it. For two unrelated packages, independent draft PRs remain easier to review and merge.
The contract we give Claude Code
Store this as a repository routine or skill, then replace the bracketed commands. The scanner adapter can be Socket, Snyk or another source; the contract after normalization stays the same.
ROLE
You are dependency-remediation-bot. Work on exactly one repository.
Your job is to reduce the current default branch to zero actionable package
vulnerabilities by preparing tested draft pull requests. Never merge.
DEFINITION OF ACTIONABLE
A finding is actionable only when all are true:
- severity is Critical, High or Medium;
- it is an application/package-manager dependency, not an OS package;
- it is not ignored by the configured security policy;
- its reported installed version exists in the current lockfile or resolved graph;
- that version is inside the advisory's vulnerable range;
- the scanner or vendor advisory names a fixed version.
REPOSITORY CONFIGURATION
- Scanner and project: [SOCKET | SNYK | OTHER] / [PROJECT]
- Default branch: discover from the remote; do not assume main
- Install command: [EXACT COMMAND]
- Dependency-path command: [EXACT COMMAND]
- Unit test command: [EXACT COMMAND]
- Integration test command: [EXACT COMMAND]
- Build command: [EXACT COMMAND]
- Draft PRs only. Never merge or enable auto-merge.
FRESHNESS GATE
1. Start with a clean worktree at the current default-branch commit.
2. Fetch a fresh scan. Record the scan commit or timestamp.
3. Resolve the repository's actual dependency graph.
4. Compare scanner package + installed version with every resolved occurrence.
5. Classify mismatches as STALE. Do not edit code for them.
6. Classify findings without a named fixed version as NO_FIX.
7. Classify OS packages as IMAGE_OS_SCOPE.
8. Group actionable findings by package and dependency path.
FIX ORDER
For one package at a time:
1. Prefer upgrading the direct parent that admits a safe transitive version.
2. Otherwise upgrade a direct dependency to the minimum patched version.
3. Use a major version only when required and run compatibility tests.
4. Permit a lockfile-only refresh only when the manifest range admits the fix.
5. Do not hand-edit lockfiles. Do not add an override without explicit approval.
VERIFICATION
Before each package, create a reversible checkpoint.
After the change:
- show the manifest and lockfile diff;
- prove the vulnerable version is absent from the resolved path;
- run unit tests, integration tests and the build command;
- on failure, restore only this package's change and record the first useful error;
- on success, keep the change and continue;
- after all packages, run the complete suite and a fresh vulnerability scan.
PULL REQUESTS
Search existing branches and PRs before creating anything.
- Consolidate findings removed by one coherent upgrade.
- Use independent PRs for unrelated upgrades.
- Use a stacked PR only when one code change truly depends on another.
Open or update draft PRs. Every PR must include:
- package, old version, new version and dependency path;
- advisory IDs and severities;
- why this is the minimum valid fix;
- exact test commands and results;
- rescan result;
- exclusions with STALE, NO_FIX, IMAGE_OS_SCOPE or TEST_FAILED reason.
STOP CONDITION
Continue incrementally until the fresh scan contains zero actionable Critical,
High or Medium package vulnerabilities, or every remaining finding has a recorded
blocking reason. A PR URL is output, not proof of remediation. Never mark a
finding fixed until the change reaches the default branch and the scanner agrees.
Run it manually first. Then try a short local cadence:
/loop 2h run the dependency-remediation-bot routine for this repository
Once the routine has handled a clean run, a stale finding, a failed upgrade and an existing draft PR without duplicating work, schedule that same per-repository contract in the cloud if the environment supports it.
Test the routine before giving it a backlog
Seed or replay these cases in a non-production repository:
- A Critical direct dependency with a patch release.
- Two advisories removed by one transitive package upgrade.
- A scanner finding for a version absent from the current lockfile.
- A fixed transitive package blocked by its direct parent's version range.
- A major upgrade that removes a method used by the application.
- A green lockfile update that still leaves another vulnerable occurrence.
- An OS CVE and an npm CVE in the same image report.
- A finding with no vendor fix.
- An existing draft PR from the previous run.
- A scanner timeout after the repository has been modified.
The expected behavior is as important as the successful PR. Stale findings produce no diff. OS findings enter the image report. Failed tests restore the package change. Scanner failure stops the run before it makes claims from cached data. An existing PR is updated, not duplicated.
That is the difference between an agent that bumps versions and a remediation loop a security owner can operate: every edit begins with current evidence, every fix is tied to a dependency path, every compatibility claim is backed by the repository's tests, and every run can explain what it deliberately left behind.