<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rishi Gulati</title>
    <description>The latest articles on DEV Community by Rishi Gulati (@firish).</description>
    <link>https://dev.to/firish</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3999260%2F0d126494-689b-471d-81a3-8bca21968e90.png</url>
      <title>DEV Community: Rishi Gulati</title>
      <link>https://dev.to/firish</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/firish"/>
    <language>en</language>
    <item>
      <title>Syncing Claude Code and Codex sessions across machines with git</title>
      <dc:creator>Rishi Gulati</dc:creator>
      <pubDate>Tue, 22 Sep 2026 03:58:04 +0000</pubDate>
      <link>https://dev.to/firish/syncing-claude-code-and-codex-sessions-across-machines-with-git-1gm8</link>
      <guid>https://dev.to/firish/syncing-claude-code-and-codex-sessions-across-machines-with-git-1gm8</guid>
      <description>&lt;p&gt;I work across three laptops. Claude Code and Codex both keep every conversation on the machine that started it, which means the session where the agent finally understands your codebase is stuck wherever you happened to be sitting last Tuesday.&lt;/p&gt;

&lt;p&gt;Copilot's answer is account-level cloud sync. I didn't want transcripts of my code sitting in a vendor's cloud, and I didn't want to run a server or pay for S3 either. The infrastructure I already own, trust, and use daily is a git remote. So I built &lt;a href="https://github.com/firish/repo-sessions" rel="noopener noreferrer"&gt;repo-sessions&lt;/a&gt;: sessions sync through one private git repo (a "vault"), and the sync triggers ride the git push and pull you already do. Push on laptop A. On laptop B, pull, then &lt;code&gt;claude --resume&lt;/code&gt;. The conversation is there.&lt;/p&gt;

&lt;p&gt;This post is the story of building it, told mostly through three bugs. Each one looked like a small edge case and turned out to be structural.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can't just copy the files
&lt;/h2&gt;

&lt;p&gt;Claude Code stores each session as a JSONL file under &lt;code&gt;~/.claude/projects/&amp;lt;munged-path&amp;gt;/&amp;lt;session-uuid&amp;gt;.jsonl&lt;/code&gt;. The munged path is your project's absolute path with every character outside &lt;code&gt;[A-Za-z0-9-]&lt;/code&gt; replaced by a dash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mungeCurrent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;absPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;/Users/rishi/dev/proj&lt;/code&gt; becomes &lt;code&gt;-Users-rishi-dev-proj&lt;/code&gt;. On top of that, every single line of the transcript embeds your &lt;code&gt;cwd&lt;/code&gt;, plus the session id, tool version, and git branch.&lt;/p&gt;

&lt;p&gt;Two consequences. First, resume looks sessions up by the munged path of the directory you're standing in, so on another machine (different username, different clone location) the file is in a directory Claude Code will never compute. Second, even if you put the file in the right place, the embedded paths point at directories that don't exist there.&lt;/p&gt;

&lt;p&gt;I verified all of this empirically before writing any real code, because none of it is documented. The method: start a session that establishes a codeword, hand-copy the transcript to a second location with paths rewritten, and try to resume. The negative control ran first: resuming on "machine B" without the rewrite says &lt;code&gt;No conversation found&lt;/code&gt;, which is exactly the failure this tool exists to fix. With the rewrite, the resumed session recalled the codeword and reported machine B's path as its cwd.&lt;/p&gt;

&lt;p&gt;The spike also surfaced something uncomfortable: the munge rule has drifted across Claude Code versions. The same directory on my machine had transcripts under both &lt;code&gt;-AI-AI-ML&lt;/code&gt; and &lt;code&gt;-AI-AI_ML&lt;/code&gt;, meaning some older build preserved underscores. So the locator scans all known rule variants instead of computing one directory name and trusting it. When you build on undocumented formats, you inherit their history too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;On push, every absolute path in the transcript becomes a token: &lt;code&gt;${CSS_PROJECT_ROOT}&lt;/code&gt;, &lt;code&gt;${CSS_HOME}&lt;/code&gt;, and the munged dirname (which shows up whenever tool output references &lt;code&gt;~/.claude/projects/...&lt;/code&gt;). On pull, tokens are rewritten for the local machine. Codex needs a third path surface, because its rollout files contain self-references to &lt;code&gt;$CODEX_HOME&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The vault is deliberately boring: one private git repo, plain directories, one namespace per project keyed by a hash of the normalized origin URL. Any git UI can inspect it. Sync triggers are git hooks (&lt;code&gt;pre-push&lt;/code&gt; sends sessions out, &lt;code&gt;post-merge&lt;/code&gt; and &lt;code&gt;post-checkout&lt;/code&gt; bring them in), and existing hooks are chain-loaded first with their stdin and argv intact, so your husky setup keeps working and its failures still abort the git operation. Mine never do: if the vault is unreachable, everything behaves as if the tool doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug one: Windows backslashes inside JSON
&lt;/h2&gt;

&lt;p&gt;Symptom, found by dogfooding on my actual Windows laptop: a synced session opens, but only the last few turns render.&lt;/p&gt;

&lt;p&gt;Cause: transcripts are JSON, and I was substituting paths into raw bytes. Splice &lt;code&gt;C:\Users\rishi\dev\proj&lt;/code&gt; into a JSON string literal and you've written &lt;code&gt;\U&lt;/code&gt;, which is not a valid escape sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ls C:&lt;/span&gt;&lt;span class="se"&gt;\U&lt;/span&gt;&lt;span class="s2"&gt;sers&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s2"&gt;ishi&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="s2"&gt;ev&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="s2"&gt;roj"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unparseable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;line&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ls C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;rishi&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;dev&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;proj"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;should&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;disk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code fails to parse the corrupt lines, the parent-uuid chain linking turns together breaks, and the UI shows you whatever survived. So all transcript substitution now works on the JSON-escaped spellings: the tokenizer matches every spelling of a path (escaped, forward-slash, either drive-letter case, longest first so escaped forms are consumed before their shorter cousins), and rehydration emits the escaped form.&lt;/p&gt;

&lt;p&gt;Same family, same week: Windows git's &lt;code&gt;autocrlf&lt;/code&gt; rewrote LF vault files to CRLF on checkout, which read as phantom content changes in synced memory files (fix: &lt;code&gt;* -text&lt;/code&gt; in the vault plus EOL-insensitive comparison). And git's default 1 MiB &lt;code&gt;http.postBuffer&lt;/code&gt; was rejecting multi-megabyte transcript pushes over https with an opaque HTTP 400, so background pushes failed silently, the worst possible failure mode for a sync tool.&lt;/p&gt;

&lt;p&gt;The lesson so far: the same content has more byte representations across machines than you think. I thought I'd learned it. The next bug was the same lesson with teeth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug two: the session that quoted its own sync metadata
&lt;/h2&gt;

&lt;p&gt;A month in, one session started reading as permanently diverged on every pull, on both machines, even when I knew nothing had changed. Naturally it was the session where I was using Claude to debug this tool.&lt;/p&gt;

&lt;p&gt;Here's the mechanism. Tokenization is machine-relative: my Mac's tokenizer knows the Mac's paths. But this transcript &lt;em&gt;quoted&lt;/em&gt; my Windows machine's path spellings as ordinary message text, pasted terminal output, a munged dirname like &lt;code&gt;C--Users-rishi-...&lt;/code&gt;, chunks of a metadata file. When the Mac pushed, those strings went into the vault verbatim, because to the Mac they're not paths, just text. The Windows machine's tokenizer would have collapsed them. So no tokenization the Mac can ever perform reproduces the vault bytes, the hashes disagree forever, and the sync layer concludes the session has diverged.&lt;/p&gt;

&lt;p&gt;Then it got worse. The reconcile command finds the common prefix of the two copies at the byte level. The first quoted spelling appeared around line 62, so it found a false fork point there and spliced everything after it back on as a "divergent tail". Each run of the repair grew the transcript: 471 lines, then 881, then 1701, with some turns duplicated eight times. Watching a repair command make the patient worse is a special feeling.&lt;/p&gt;

&lt;p&gt;The fix has two parts. Every push now records that device's path context (project root, home, tool data dir) in the vault index. And comparisons never touch raw bytes: both sides get folded through the tokenizer under every device context the vault has seen, in a deterministic order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canonForCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctxs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PathCtx&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SubstOpts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripVolatile&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stripVolatile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ctxs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokens are inert under further tokenization, so the fold is a projection, applying it twice equals applying it once. That property is a unit test, straight from the incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;once&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonForCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claudeAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctxs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;twice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonForCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claudeAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctxs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the fold genuinely collapses foreign spellings&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twice&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// and it is a projection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just as important: the normal form is compare-only. The vault always stores the writer's own tokenization and installs always rehydrate the raw stored bytes. Comparison logic can evolve without ever rewriting anyone's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug three: the tool edits the transcript behind your back
&lt;/h2&gt;

&lt;p&gt;Weeks later, false divergence came back, and this time the transcript quoted no foreign paths at all.&lt;/p&gt;

&lt;p&gt;It turned out Claude Code appends a title-refresh record (&lt;code&gt;{"type":"ai-title",...}&lt;/code&gt;) roughly every turn, on whichever machine is currently live. Two honest copies of the same conversation end up with different numbers of these records at different positions. Content-wise one copy was a clean prefix of the other, just behind. Byte-wise, diverged.&lt;/p&gt;

&lt;p&gt;So the normal form grew a second axis: before the path fold, adapters can strip records the tool rewrites non-deterministically. The strip is parse-confirmed, only a line whose top-level JSON type is &lt;code&gt;ai-title&lt;/code&gt; is dropped, so a turn that quotes such a record as data survives. Which mattered immediately, because the debugging session quoted them constantly.&lt;/p&gt;

&lt;p&gt;The general lesson landed harder than either bug alone: a compare normal form has to normalize every axis of benign nondeterminism, and every new record type the vendor ships can reintroduce this bug class. That's the treadmill you're on when you build against undocumented formats. My concession to it is a doctor command that verifies drift the only way that can't lie: by actually resuming a session and checking the tool responds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that survived
&lt;/h2&gt;

&lt;p&gt;Three principles came out of this intact and now gate every feature:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transcripts are append-only.&lt;/strong&gt; Prefix means fast-forward, divergence means conflict copies, and &lt;code&gt;chat rebase&lt;/code&gt; splices tails back into one transcript ("meanwhile, on the other laptop", which is the truth). Nothing is ever silently dropped. Rewind is a fork from vault history, never an in-place truncation, because other devices would just push the "missing" turns right back. Deleted sessions restore from vault history, since the vault is git and every synced state is a commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never write another tool's database.&lt;/strong&gt; Codex indexes sessions in SQLite alongside the rollout files. I don't touch it. Drop a rewritten rollout into the date tree and the index heals itself on first resume. Slightly worse UX (the interactive picker lists a synced session only after you've resumed it by id once), massively better failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Degrade to invisible.&lt;/strong&gt; Vault unreachable, tool not set up, repo not enabled: every hook exits quietly and the coding tools behave exactly as if repo-sessions were never installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; repo-sessions
chat setup   &lt;span class="c"&gt;# once per machine: creates or reuses your private vault&lt;/span&gt;
chat init    &lt;span class="c"&gt;# once per repo: from here git push/pull carry your sessions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then work normally. Sessions and per-project memory follow the repo. &lt;code&gt;chat list&lt;/code&gt;, &lt;code&gt;chat resume &amp;lt;name&amp;gt;&lt;/code&gt;, &lt;code&gt;chat rebase&lt;/code&gt; when you got ahead of yourself on two machines at once, &lt;code&gt;chat restore&lt;/code&gt; when you deleted something you shouldn't have.&lt;/p&gt;

&lt;p&gt;(The binary is named &lt;code&gt;chat&lt;/code&gt;. macOS ships a pppd utility from the dial-up era at &lt;code&gt;/usr/sbin/chat&lt;/code&gt;, and a shell opened before the install will happily run that one instead, which blocks on stdin and looks exactly like "the tool does nothing". &lt;code&gt;hash -r&lt;/code&gt;. Ask me how I know.)&lt;/p&gt;

&lt;p&gt;It's MIT licensed, TypeScript with zero runtime dependencies, 61 hermetic tests running on a 3-OS CI matrix, and dogfooded daily across three machines, including a 400k-token session that resumed cross-OS with an accurate recap. One honest caveat: transcripts contain whatever your session saw. The vault must be private (setup refuses public GitHub vaults), pushes run a gitleaks-style secret scan, and there's a per-session ignore file, but you should treat the vault the way you treat &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you to steal
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Byte equality is the wrong identity for synced content. You need equality up to every writer's encoding, and you should expect the set of writers to grow.&lt;/li&gt;
&lt;li&gt;If synced content can quote your own sync metadata back at you, it eventually will. Any session about the tool itself is a self-referential input, and dogfooding guarantees those exist.&lt;/li&gt;
&lt;li&gt;Normalize at compare time, never at rest. A compare-only normal form can evolve freely because it never rewrites stored data.&lt;/li&gt;
&lt;li&gt;When there are no docs, a codeword and a negative control settle in one evening what speculation never will.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/firish/repo-sessions" rel="noopener noreferrer"&gt;https://github.com/firish/repo-sessions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, the failure modes I haven't found yet are the ones I most want to hear about.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Claude Code in Visual Studio: now it drives the debugger, catches flaky tests, and reads library source</title>
      <dc:creator>Rishi Gulati</dc:creator>
      <pubDate>Wed, 08 Jul 2026 18:41:05 +0000</pubDate>
      <link>https://dev.to/firish/claude-code-in-visual-studio-now-it-drives-the-debugger-catches-flaky-tests-and-reads-library-35b6</link>
      <guid>https://dev.to/firish/claude-code-in-visual-studio-now-it-drives-the-debugger-catches-flaky-tests-and-reads-library-35b6</guid>
      <description>&lt;p&gt;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 &lt;a href="https://github.com/anthropics/claude-code/issues/15942" rel="noopener noreferrer"&gt;open issue&lt;/a&gt; asking for Visual Studio support with a few hundred upvotes and no ETA, so I built the Visual Studio side myself.&lt;/p&gt;

&lt;p&gt;It is a native VS 2026 extension. The important thing to say up front is what it is &lt;em&gt;not&lt;/em&gt;: it is not another agent. The &lt;code&gt;claude&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdxhgff2rbulkairvmss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdxhgff2rbulkairvmss.png" alt="A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback" width="799" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That part was the plan. The part I did not expect is what became possible &lt;em&gt;once the IDE half existed&lt;/em&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching the code run, instead of reading it
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj772pssez5eq3tgrklr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj772pssez5eq3tgrklr.png" alt="A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback" width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje6o4h4z5d14wt11wih6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje6o4h4z5d14wt11wih6.png" alt="A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback" width="800" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bug is that the combo multiplier never resets on a zero round. &lt;code&gt;if (points &amp;gt; 0)&lt;/code&gt; and &lt;code&gt;else if (points &amp;lt; 0)&lt;/code&gt; both miss &lt;code&gt;points == 0&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  It grew past stepping
&lt;/h2&gt;

&lt;p&gt;Once the drive loop worked, the rest followed. Claude can now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attach to a process that is already running.&lt;/strong&gt; 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 &lt;code&gt;curl&lt;/code&gt;, and stops at the throw site inside the handler instead of the generic &lt;code&gt;catch&lt;/code&gt; that swallowed it into a bland 500.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Untangle a deadlock.&lt;/strong&gt; 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 &lt;code&gt;A -&amp;gt; B -&amp;gt; C -&amp;gt; A&lt;/code&gt; lock cycle, correctly ignoring the threads that are merely idle or busy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the one I am most proud of, because Visual Studio's own UI cannot even set it through an automation API: &lt;strong&gt;data breakpoints.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvm2urr46q1x8c5i1v96.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvm2urr46q1x8c5i1v96.png" alt="Claude arming a plain, conditional, and multi-watch data breakpoint in one call, with the vs_set_data_breakpoint tool use" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The conditional watch broke exactly on the write that corrupted the total, and the plain watch recorded the full timeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;value (cents)&lt;/th&gt;
&lt;th&gt;written by&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1-3&lt;/td&gt;
&lt;td&gt;0 → 30000 → 42000 → 54000&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddItems&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;three line items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;54000 → 48600&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ApplyBulkDiscount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10% off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;48600 → 52488&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ApplyTax&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;+8% tax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;52488 → &lt;strong&gt;-47512&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ApplyLoyaltyCredit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the corruption&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching a flaky test red-handed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnhuvyzfh449kocyy13g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnhuvyzfh449kocyy13g.png" alt="A Claude edit open in the Visual Studio diff viewer with Accept, Reject, and Reject with feedback" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet test&lt;/code&gt; runs your tests. What it cannot do is stop &lt;em&gt;inside&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9y5zxiqorrqcdr7iy7s5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9y5zxiqorrqcdr7iy7s5.png" alt="The Visual Studio debugger paused inside a flaky test at the throw site, with the exception live in the Locals window" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;if (Random.Shared.Next(3) == 0)&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;Reflection.Emit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading code the way the compiler does
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;grep&lt;/code&gt; can never tie back to the interface.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it hangs together
&lt;/h2&gt;

&lt;p&gt;If you like the plumbing: the extension starts a localhost WebSocket server, writes a lockfile, and launches &lt;code&gt;claude&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

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

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace:&lt;/strong&gt; &lt;a href="https://marketplace.visualstudio.com/items?itemName=firish.bridgev1" rel="noopener noreferrer"&gt;Claude Code for Visual Studio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source and docs:&lt;/strong&gt; &lt;a href="https://github.com/firish/claude_code_vs" rel="noopener noreferrer"&gt;github.com/firish/claude_code_vs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
      <category>visualstudio</category>
    </item>
    <item>
      <title>I brought Claude Code to Visual Studio, then handed it the debugger</title>
      <dc:creator>Rishi Gulati</dc:creator>
      <pubDate>Tue, 23 Jun 2026 19:00:23 +0000</pubDate>
      <link>https://dev.to/firish/i-brought-claude-code-to-visual-studio-then-handed-it-the-debugger-4k5</link>
      <guid>https://dev.to/firish/i-brought-claude-code-to-visual-studio-then-handed-it-the-debugger-4k5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; this extension has grown well past what this post covers. It now has a debugger Claude can drive, data breakpoints, test integration, and Roslyn code navigation. The current, fuller walkthrough is here: &lt;a href="https://dev.to/firish/claude-code-in-visual-studio-now-it-drives-the-debugger-catches-flaky-tests-and-reads-library-35b6"&gt;I brought Claude Code to Visual Studio, and gave it a debugger it can drive&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Plenty of times I've run a C# project in VS Code instead of Visual Studio, not because I like it better, but because that's where Claude could show me its edits in a real diff before I accept or reject. There's already &lt;a href="https://github.com/anthropics/claude-code/issues/15942" rel="noopener noreferrer"&gt;an open request for something similar on GitHub&lt;/a&gt; with a stack of upvotes and no answer yet. So, I built the missing half myself.&lt;/p&gt;

&lt;p&gt;It started as a diff window. I wanted Claude's edits to show up in the real Visual Studio diff instead of a yes/no prompt in a terminal. That worked, and so, I moved to the next biggest inconvenience, letting the Claude CLI read the compiler errors and warnings, the lint problems, and the rest of the build output directly, instead of copy-pasting them in. This was the v1 of my project.&lt;/p&gt;

&lt;p&gt;The next biggest problem (and opportunity) came when I found myself using the debugger, and adding console logs and then hand-feeding those values to the Claude CLI to get it to debug a tricky issue with the correct context. So I started building a bridge between Visual Studio's debugger automation and the CLI. A few weeks later Claude was setting breakpoints in my files, stepping through them, watching variables mutate at different frames, and telling me where a bug was hiding that it would have probably skimmed past while reading my code.&lt;/p&gt;

&lt;p&gt;Quick disclaimer first. This is an unofficial community project, not affiliated with Anthropic. It's open source, it targets Visual Studio 2026, and it needs the Claude Code CLI installed. The Anthropic CLI is the hero doing the actual model work. My extension is the IDE sidekick supporting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run that made me excited
&lt;/h2&gt;

&lt;p&gt;Check out this scoring function. Each round scores points, and a run of scoring rounds builds a combo multiplier. The first hit counts once, the second in a row counts double, and so on. A round that scores nothing should break the run and reset the combo.&lt;/p&gt;

&lt;p&gt;It compiles. It runs. It prints the wrong number. Give it the rounds &lt;code&gt;{5, 3, 0, 4, 2, 0, 6}&lt;/code&gt; and it returns 61, when the answer is 25. Nothing in the source looks wrong. No IDE warnings.&lt;/p&gt;

&lt;p&gt;I opened a fresh Claude session that had never seen the file. Then I asked it to find what was tripping up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj772pssez5eq3tgrklr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj772pssez5eq3tgrklr.png" alt="Paused inside the loop, with the locals sitting right there" width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It stepped through the rounds one at a time, reading the combo and the running total at each stop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxeqkuhkpz9zj8vzy9yo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxeqkuhkpz9zj8vzy9yo.png" alt="Claude driving the debugger, step by step" width="800" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the trace it built as it went:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;round&lt;/th&gt;
&lt;th&gt;points&lt;/th&gt;
&lt;th&gt;combo after&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje6o4h4z5d14wt11wih6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje6o4h4z5d14wt11wih6.png" alt="The same trace, the way Claude laid it out" width="800" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Round 2 is the tell. The points are 0, so the combo should drop back to nothing. It stays at 2. Same at round 5. The run never resets, so every later round gets multiplied by a number that's too big and the total climbs to 61.&lt;/p&gt;

&lt;p&gt;Then it pointed at the cause:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybixuljim1gedvwaow06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybixuljim1gedvwaow06.png" alt="The diagnosis and the fix" width="799" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The zero case falls through both branches. The code handles points greater than zero and points less than zero, and a plain zero matches neither, so the combo never resets.&lt;/p&gt;

&lt;p&gt;The bug is invisible in the output. The only way to catch it is to watch the combo fail to reset while the program runs, which is what a person does with a debugger (or a bunch of tracing statements), and what the agent did here. It didn't read the bug. It watched it. Start to finish, about a minute and a half.&lt;/p&gt;

&lt;p&gt;Then it wrote the fix, and the fix opened where I would want it. Not as text in a terminal, but in Visual Studio's own diff viewer, with Accept and Reject right there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1s82venxuj5qc1i7t9xo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1s82venxuj5qc1i7t9xo.png" alt="Claude's fix in the native diff viewer" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The everyday part it rides on
&lt;/h2&gt;

&lt;p&gt;That diff is the part I had been leaning on for weeks before any of the debugger work, and it's still what I use most. Every change Claude proposes opens as a real diff in the editor you already trust. You read it and click Accept or Reject. There's no second confirmation waiting back in the terminal.&lt;/p&gt;

&lt;p&gt;The reject is the part I like. You don't just say no, you say why, in a sentence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdxhgff2rbulkairvmss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdxhgff2rbulkairvmss.png" alt="Rejecting an edit with a written reason" width="799" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That note goes straight back to the CLI, and Claude picks it up and tries again with your correction instead of taking another blind swing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp24s4juxterij2lm4kqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp24s4juxterij2lm4kqt.png" alt="The same note, back in the CLI, getting acted on" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It reads your build too. Errors and warnings from the Error List, C# and C++, reach the model directly, so it can see what's broken without you pasting anything in. And there's a docked panel that keeps the session honest: what you've accepted and rejected, tokens and cost, and a line for how much the agent has been inspecting the debugger versus driving it. The switch that lets it drive lives here as well, off until you turn it on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw2fpyv6mr35owwq2fisx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw2fpyv6mr35owwq2fisx.png" alt="The panel: session stats, and the switch that gates anything that runs your code" width="688" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this took real work
&lt;/h2&gt;

&lt;p&gt;The obvious approach doesn't work, and that's most of the story.&lt;/p&gt;

&lt;p&gt;Claude Code talks to an IDE over a small local protocol: a lockfile plus a localhost WebSocket speaking MCP. The catch is that the CLI is picky about which of the IDE's tools it actually shows the model. It hands over diagnostics and runs the rest itself. So I could register a new "read the debugger" tool and the model would never be allowed to touch it.&lt;/p&gt;

&lt;p&gt;The debugger gets to Claude two other ways.&lt;/p&gt;

&lt;p&gt;The first is a hook. Send a prompt while you're paused at a breakpoint and a hook fires, reads the break state out of Visual Studio, and drops it into the context before the model sees your message. No tool call. Claude starts the turn already knowing where you're stopped and what every local holds.&lt;/p&gt;

&lt;p&gt;The second is a separate MCP server, the kind you register yourself for a project. Those aren't filtered, so any tool they expose, the model can call. The extension runs a small one for the debugger and registers it on launch. The work happens inside Visual Studio. A shim just shuttles messages to it.&lt;/p&gt;

&lt;p&gt;Reading state was pretty easy. Driving the debugger was the part that kept getting errors. "Step over" isn't a question with an instant answer. You issue it, then you have to wait for the program to stop again before the result means anything. Block the UI thread to wait and you freeze the IDE. The solution is to implement the step such that it goes out without blocking the main UI thread, the extension listens for the debugger's mode-change event, holds the request, and finishes it when the next break lands. From the model's side it's one clean call that returns the new state. Underneath it's an event handshake that keeps the editor responsive.&lt;/p&gt;

&lt;p&gt;Driving is also off by default. Reading runtime state is always on, since it can't break anything. Continue, step, breakpoints, starting and stopping a session: all of that sits behind the checkbox in the panel, and it resets every time you restart Visual Studio. Letting an agent run your code is a real decision, so its off by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;A few things on the list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test driven debugging. Run the suite, and when a test fails, set a breakpoint at the failure, start debugging that test, and drive to the fault on its own.&lt;/li&gt;
&lt;li&gt;(Work in progress) Break on thrown. Stop the instant a chosen exception is thrown, at the throw site, not wherever it gets swallowed. &lt;/li&gt;
&lt;li&gt;CPU and memory. Point the .NET diagnostics tools at the running process and surface the hot methods and the heaviest allocations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's on the Visual Studio Marketplace, and the source is on GitHub. You need Visual Studio 2026 and the Claude Code CLI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Marketplace: &lt;a href="https://marketplace.visualstudio.com/items?itemName=firish.bridgev1" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=firish.bridgev1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source and docs: &lt;a href="https://github.com/firish/claude_code_vs" rel="noopener noreferrer"&gt;https://github.com/firish/claude_code_vs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you work in Visual Studio and you've wanted Claude Code there, give it a try and tell me what breaks.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
      <category>visualstudio</category>
    </item>
  </channel>
</rss>
