All projects

Project

dependency-upgrade-agent: A Claude Code Agent for Major Dependency Upgrades

A Claude Code skill that plans, codemods, and verifies major-version dependency upgrades (Spring Boot 2→3, javax→jakarta) — then proved itself on a real 6-year-stale app.

Claude CodeJavaMavenSpring BootOpenRewrite

Key highlights

  • 🪜 Incremental hop planning — never jumps straight from an old major to the latest
  • 📸 Characterization-snapshot testing for legacy code with zero pre-existing tests
  • 🔍 Found and fixed 2 real bugs during its first real run, neither of them documented anywhere
  • 🚫 Never blocks on missing tests, never auto-merges — every hop is its own reviewable commit

Real hop commits from the first dogfood run, landing directly above the project's last commit from 2020

Overview#

dependency-upgrade-agent is a Claude Code skill that automates the part of software maintenance almost everyone avoids: major-version dependency upgrades. Not mvn versions:set — the kind of upgrade that renames packages, changes framework defaults, and breaks code that compiled fine yesterday. Point it at a Java/Maven/Spring Boot project and it plans an incremental hop path, applies OpenRewrite codemods where they exist, fixes what codemods can’t, and verifies every single hop with a real build → test → boot cycle before moving to the next one.

It’s published as an installable plugin/plugin marketplace add krishanchawla/dependency-upgrade-agent and it’s live in any Claude Code session.

Why I Built This#

I had a Spring Boot app — mock-server-v1, a mock-API tool I wrote in 2020 — sitting on Spring Boot 2.3.4, an EOL release, with zero tests. Upgrading it by hand meant reading migration guides, hoping I caught every breaking change, and having no way to know if I’d silently broken something until it broke in production. That’s true of most legacy code, not just mine.

So instead of upgrading it by hand, I built the tool to do it — and then used the tool to do it, on that exact app, as the first real test.

Design: Verify, Don’t Gatekeep#

The first version of this skill refused to run on a project with no test suite — “no safety net, no upgrade.” That felt responsible until I actually thought about who needs this tool: most real org codebases don’t have tests, and a tool that only works on already-well-maintained code is solving the easy 20% of the problem.

So the design changed to a graduated verification model instead of a hard gate:

  1. Real tests exist → run them, use them as the primary signal.
  2. No tests, but a runtime surface exists (the normal case) → boot the unmodified app, capture a characterization snapshot of real request/response pairs across its main routes, and diff against that snapshot after every hop. No test-writing required from anyone — it’s throwaway scratch data, not permanent test code.
  3. Neither → compile success is the only provable signal, and the tool says so explicitly rather than implying more confidence than it has.

Whichever tier applies gets written into every hop’s commit message and the final report, so a human reviewer knows exactly how much a green result actually proves.

What It Found, Running for Real#

Running it against mock-server-v1 surfaced two bugs neither I nor any migration guide had flagged:

  • H2 1.x → 2.x reserves new keywords. H2 jumps versions inside the Spring Boot 2.7 hop itself — before the Spring Boot 3 boundary most guides warn about — and H2 2.x reserves VALUE as a keyword. A column named value broke table creation at boot. The app still started — Tomcat came up fine — so this would have been invisible to a compile-only or boot-only check. The characterization snapshot caught it because a header-writing request round-tripped differently.
  • A JPQL query referencing a column name instead of an entity field. WHERE mock_id = :mockId (should be WHERE mockId = :mockId) had been sitting in the codebase since 2020, passing silently on Hibernate 5.4. Hibernate 6 validates HQL eagerly at startup and rejected it outright: Could not interpret path expression 'mock_id'. The app wouldn’t boot at all until this was fixed.

Both went into the tool’s own reference docs immediately after — the whole point of dogfooding is that the docs get more accurate every time something surprises you.

Result#

Three hops, three commits, each independently verified:

2.3.4.RELEASE (2020, EOL) → 2.7.18 → 3.0.13 (javax→jakarta, Java 17) → 3.5.16 (current)
plaintext

Characterization snapshot diffed clean across all three hops — 9 request/response pairs, one disclosed non-regressive change (a default error-response field Spring Boot itself stopped including), zero actual regressions. Full hop-by-hop breakdown, commit links, and before/after in the mock-server-v1 case study.

What’s Not Here Yet#

  • v1 scope is Java + Maven + Spring Boot only, deliberately — that ecosystem has mature codemod tooling (OpenRewrite) to build the orchestration loop against reliably. Structured so Gradle, npm, and Python can be added as sibling ecosystems later without touching the core loop.
  • Single data point so far. It’s only been run against a project I wrote myself. The next real test is a codebase I didn’t design.
  • No automated validation of the skill itself — it’s a playbook the model follows, not code with its own test suite. Its reliability is only as proven as the runs behind it.

Source and full docs: github.com/krishanchawla/dependency-upgrade-agent

Technologies used

Claude Code
Java
Maven
Spring Boot
OpenRewrite
Search posts & projects