DEV Community

Rishi Gulati
Rishi Gulati

Posted on

Claude Code in Visual Studio: now it drives the debugger, catches flaky tests, and reads library source

Claude Code has this nice thing where it plugs into your editor. On VS Code or a JetBrains IDE, its edits open as a diff you accept or reject, it reads your compiler errors, and it knows what you have selected. On Visual Studio you get none of that. You copy and paste into a terminal like it is 2022. There is an open issue asking for Visual Studio support with a few hundred upvotes and no ETA, so I built the Visual Studio side myself.

It is a native VS 2026 extension. The important thing to say up front is what it is not: it is not another agent. The claude CLI does all the thinking. My extension is the IDE half of Claude Code's integration protocol, which I had to reverse-engineer off the wire since it is undocumented. It makes zero model calls of its own. It is a bridge.

The first thing you get is the diff gate. When Claude wants to change a file, the change opens in Visual Studio's own diff viewer, and approving there is the only step. No second yes/no prompt in the terminal. You can reject with a note, and it reconsiders.

A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback

That part was the plan. The part I did not expect is what became possible once the IDE half existed. Because I control the Visual Studio side, I could hand the agent things the CLI can never reach on its own. The one I keep coming back to is the debugger.

Watching the code run, instead of reading it

Here is a scoring function that returns the wrong total. Nothing in the source reads as wrong. The final number is just off. A fresh Claude session, with no idea what the bug was, was asked to check it, and it drove the Visual Studio debugger to find it in about ninety seconds.

A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback

It set a breakpoint at the top of the loop, started the session, and stepped through round by round, watching a combo counter. Here is the trace it kept, for the input {5, 3, 0, 4, 2, 0, 6}:

A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback

The bug is that the combo multiplier never resets on a zero round. if (points > 0) and else if (points < 0) both miss points == 0, so the streak carries across a zero and inflates the score. You cannot see that by reading the code. It only shows up when you watch the counter stay put across the round it should have cleared. That is the whole difference between reading code and debugging it, and the agent got to do the second one.

Reading runtime state is always on. Driving the debugger, actually stepping and setting breakpoints and running your code, is opt-in behind a toggle in the panel that resets every session, because it runs your program under the model's control.

It grew past stepping

Once the drive loop worked, the rest followed. Claude can now:

  • Attach to a process that is already running. A hosted web service or a desktop app, not just an F5 launch. It lists the processes, attaches, arms a first-chance exception, triggers the request itself with curl, and stops at the throw site inside the handler instead of the generic catch that swallowed it into a bland 500.
  • Untangle a deadlock. A deadlocked program never hits a breakpoint, so a breakpoint is useless. Claude pauses the hung process, reads each stuck thread's wait chain, and reconstructs the exact A -> B -> C -> A lock cycle, correctly ignoring the threads that are merely idle or busy.

And the one I am most proud of, because Visual Studio's own UI cannot even set it through an automation API: data breakpoints. Point Claude at a field and it stops the instant that field is written, or records every value the field takes, in order. It can watch conditionally (break only when the value goes below zero) and watch several fields at once.

I built a little fixture where an order's total is mutated through four different pricing methods and comes out negative, with no single line to breakpoint. Claude armed a plain watch, a conditional watch, and a second watch at once:

Claude arming a plain, conditional, and multi-watch data breakpoint in one call, with the vs_set_data_breakpoint tool use

The conditional watch broke exactly on the write that corrupted the total, and the plain watch recorded the full timeline:

# value (cents) written by
1-3 0 → 30000 → 42000 → 54000 AddItems three line items
4 54000 → 48600 ApplyBulkDiscount 10% off
5 48600 → 52488 ApplyTax +8% tax
6 52488 → -47512 ApplyLoyaltyCredit the corruption

Getting that to work was the hardest part of the whole project. There is no EnvDTE surface for it, so it rides a small Concord debug-engine component that ships with the extension and arms the breakpoint from inside the engine, then streams the changes back out over file IPC.

Catching a flaky test red-handed

A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback

dotnet test runs your tests. What it cannot do is stop inside a failing one in the debugger, or reproduce an intermittent failure on purpose and pause on it. So I wired Visual Studio's Test Explorer engine to the same debug session.

The signature move is catching a heisenbug. Point it at a flaky test and it loops the test under the debugger with break-on-thrown armed, lets the passing runs finish, and stops on the first failing run, paused at the throw with the exception live in the frame:

The Visual Studio debugger paused inside a flaky test at the throw site, with the exception live in the Locals window

The honest twist in that run: when it read the frame, it found there were no captured locals at all, and the throw came straight out of an if (Random.Shared.Next(3) == 0) branch. The test was not exposing a product bug. The test itself was nondeterministic because it branched on an unseeded RNG. A re-run loop would have given you a different red line every time and never told you that. Being paused inside the exact failing run did.

Getting real per-test results out of the engine took a trick too. Its result callback is an internal interface you cannot implement in normal C#, so the extension emits a type that implements it at runtime with Reflection.Emit.

Reading code the way the compiler does

The last piece is less flashy but I use it constantly. Instead of grepping text for "where is this used," Claude asks Visual Studio's compiler (Roslyn) for the resolved answer: find-all-references, go-to-definition, find-implementations, and call and type hierarchies, resolved through interfaces, overrides, and overloads. It catches the cases text search gets wrong, like an explicit interface implementation or a virtual dispatch a grep can never tie back to the interface.

Tool What it caught that a text search misses
find-implementations Triangle's explicit IShape.Area, which .Area() or : IShape never ties back to the interface
type-hierarchy Circle and Square reach IShape transitively through ShapeBase; grep on : IShape finds only the two direct implementers
call-hierarchy the interface-dispatched callers of IShape.Area, back to Main, that a search on the concrete Circle.Area would miss
go-to-definition the right Describe overload at a call site, not all three declarations
decompile the real body of Enumerable.Sum from the library, with Math.Sqrt falling back to SourceLink

It also does the one thing reading your repo fundamentally cannot: it decompiles the body of a method that lives in a referenced DLL. Point it at a framework or NuGet call and it hands back the real C#, the way Go-To-Definition does, and for core .NET types it fetches the actual runtime source over SourceLink.

How it hangs together

If you like the plumbing: the extension starts a localhost WebSocket server, writes a lockfile, and launches claude already connected. The diff, diagnostics, and selection tools ride Claude Code's IDE protocol directly. The debugger, the test runner, and the semantic tools ride two extra MCP servers the CLI reaches through a tiny stdio shim, because that channel is the open plugin door where the model actually sees the tools. Everything runs in-process against the live Visual Studio services, on the UI thread where it has to be. The CLI does all the agent work; the extension never calls a model.

The honest part

  • Visual Studio 2026 only for now. A 2022 backfill is possible if people want it.
  • The integration protocol is undocumented and could change on any CLI update, so I pin and smoke-test against a known-good version.
  • The debugger, test, and data-breakpoint features are .NET-focused.
  • It is free, MIT, and not affiliated with Anthropic. "Claude" and "Claude Code" are Anthropic's.

If you write C# and use Claude Code, I would genuinely like to know how it holds up on a real codebase.

The deep-dives (the full debugger tool list, the test loop, the semantic surface) are in the repo's docs/ folder if you want to see how far each piece goes.

Top comments (0)