← Back to dashboard
visual-studiotoolingfresh

Visual Studio Integration Guide

What is Visual Studio?

The real model

Two extensibility worlds — out-of-process SDK (async, hot-reload) over in-process VSSDK/MEF (COM, deep editor hooks).

Start with `Microsoft.VisualStudio.Extensibility.Sdk` (NuGet, net8.0-windows): one `Extension : [VisualStudioContribution]` class is your manifest-in-code, `Command` subclasses run async against `this.Extensibility.Editor().EditAsync(...)`, and the `.Build` package packs the `.vsix` straight from `dotnet build` — F5 hot-loads into the Experimental Instance with no restart. Drop to VSSDK/MEF only for hooks the SDK doesn't expose yet: `IClassifierProvider`, `ITaggerProvider`, `AdornmentLayerDefinition`, gated by `[ContentType]`. Extensions ship as NuGet/VSIX, never npm — so the tech-stack freshness checker can't track them and the frontmatter `packages` list stays empty. For codeAmani, VS is the .NET/C++/MAUI backend tool; VS Code stays the day-to-day editor for the Next.js + M-Pesa stack, and any VS-only workflow should be documented so it isn't a hidden, bandwidth-heavy prerequisite.

Six Visual Studio editor primitives

One modern SDK, four classic MEF editor hooks, and the VSIX that ships them.

Text
██╗   ██╗██╗███████╗██╗   ██╗ █████╗ ██╗         ███████╗████████╗██╗   ██╗██████╗ ██╗ ██████╗
██║   ██║██║██╔════╝██║   ██║██╔══██╗██║         ██╔════╝╚══██╔══╝██║   ██║██╔══██╗██║██╔═══██╗
██║   ██║██║███████╗██║   ██║███████║██║         ███████╗   ██║   ██║   ██║██║  ██║██║██║   ██║
╚██╗ ██╔╝██║╚════██║██║   ██║██╔══██║██║         ╚════██║   ██║   ██║   ██║██║  ██║██║██║   ██║
 ╚████╔╝ ██║███████║╚██████╔╝██║  ██║███████╗    ███████║   ██║   ╚██████╔╝██████╔╝██║╚██████╔╝
  ╚═══╝  ╚═╝╚══════╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝    ╚══════╝   ╚═╝    ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝

Visual Studio Integration Guide

Focus: Extending the Visual Studio IDE's editor and tooling — building VSIX extensions with the modern out-of-process VisualStudio.Extensibility SDK and the classic VSSDK + MEF editor model (classifiers, adornments, taggers), and driving builds/tests from Claude Code via MSBuild and the dotnet/devenv CLIs.

Overview

Visual Studio is Microsoft's full Windows IDE for .NET, C++, web, and cross-platform mobile development. Unlike VS Code (a separate, lighter editor with a JavaScript extension model), Visual Studio extensions are .NET assemblies packaged as a VSIX and published to the Visual Studio Marketplace.

Here's the big picture to keep you oriented — pick your model, scaffold, then let the build tools package the .vsix:

There are two extensibility models, and knowing which you're in saves hours:

ModelProcessAPI styleUse it when
VisualStudio.Extensibility SDK (new)Out-of-processAsync, [VisualStudioContribution], hot-reloadNew commands, editor listeners, tool windows, LSP — start here
VSSDK + MEF (classic)In-process (COM)[Export]/[Import], requires VS restartDeep editor hooks: IClassifier, adornments, taggers, IntelliSense not yet in the new SDK

Claude Code's role here is codegen + build orchestration: scaffold the extension classes that match the fetched API, then drive dotnet build / msbuild / dotnet test to compile, package, and validate the .vsix.

Official Documentation


NuGet Packages (tracked manually — not npm/pypi)

The freshness checker only follows npm and PyPI, so the frontmatter packages list is empty. Track these by hand:

PackagePurposeVersion at review
Microsoft.VisualStudio.Extensibility.SdkCore out-of-process SDK17.14.40608
Microsoft.VisualStudio.Extensibility.BuildMSBuild targets that pack the .vsix17.14.40608
Microsoft.VisualStudio.SDK (classic)Meta-package for VSSDK/MEF editor APIsmatches your VS version (e.g. 17.x)

Latest versions: https://www.nuget.org/packages/Microsoft.VisualStudio.Extensibility.Sdk


Prerequisites

Text
Visual Studio 2022 17.9+  with the "Visual Studio extension development" workload
.NET 8 SDK

Project file

The SDK targets net8.0-windows and pulls two NuGet packages. The .Build package wires the VSIX packaging into dotnet build automatically — no source.extension.vsixmanifest hand-editing:

XML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>12</LangVersion>
    <NeutralLanguage>en-US</NeutralLanguage>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.VisualStudio.Extensibility.Sdk"   Version="17.14.40608" PrivateAssets="all" />
    <PackageReference Include="Microsoft.VisualStudio.Extensibility.Build" Version="17.14.40608" PrivateAssets="all" />
  </ItemGroup>
</Project>

Extension entry point

Every extension has one class deriving from Extension, decorated with [VisualStudioContribution]. This is your manifest-in-code:

CSHARP
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.Extensibility;

[VisualStudioContribution]
public class InsertGuidExtension : Extension
{
    public override ExtensionConfiguration ExtensionConfiguration => new()
    {
        Metadata = new(
            id: "InsertGuid.c5481000-68da-416d-b337-32122a638980",
            version: this.ExtensionAssemblyVersion,
            publisherName: "codeAmani",
            displayName: "Insert Guid Sample Extension",
            description: "Inserts a GUID at the caret in the active document."),
    };

    protected override void InitializeServices(IServiceCollection serviceCollection)
    {
        base.InitializeServices(serviceCollection);
        // Register your own services for DI here.
    }
}

A command

Commands are Command subclasses, also marked [VisualStudioContribution]. They run async against the out-of-process Extensibility object:

CSHARP
[VisualStudioContribution]
public class InsertGuidCommand : Command
{
    public override CommandConfiguration CommandConfiguration => new("%InsertGuid.DisplayName%")
    {
        Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
        Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
    };

    public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken ct)
    {
        var textView = await context.GetActiveTextViewAsync(ct);
        if (textView is null) return;

        await this.Extensibility.Editor().EditAsync(batch =>
        {
            var doc = textView.Document.AsEditable(batch);
            doc.Replace(textView.Selection.Extent, Guid.NewGuid().ToString());
        }, ct);
    }
}

Build & run

Bash
# From the extension project dir — the .Build package produces the .vsix
dotnet build -c Release

# F5 in Visual Studio launches the VS Experimental Instance with the extension
# hot-loaded (no restart). From CLI, install the packaged VSIX:
"%VsInstallDir%\Common7\IDE\VSIXInstaller.exe" bin\Release\MyExtension.vsix

Setup — Classic VSSDK + MEF (deep editor hooks)

When you need an editor hook the new SDK doesn't expose yet — syntax classification, adornments, taggers — use the in-process MEF model. Create a VSIX Project (C# › Extensibility), then add an Editor Classifier item template.

MEF is the wiring: you [Export] a provider and Visual Studio [Import]s it. The editor discovers your component by the exported interface + ContentType.

A classifier (colors text)

CSHARP
[Export(typeof(IClassifierProvider))]
[ContentType("text")]
internal class EditorClassifierProvider : IClassifierProvider
{
    [Import] internal IClassificationTypeRegistryService ClassificationRegistry { get; set; }

    public IClassifier GetClassifier(ITextBuffer buffer) =>
        buffer.Properties.GetOrCreateSingletonProperty(
            () => new EditorClassifier(ClassificationRegistry));
}

internal class EditorClassifier : IClassifier
{
    private readonly IClassificationType _type;
    internal EditorClassifier(IClassificationTypeRegistryService registry) =>
        _type = registry.GetClassificationType("EditorClassifier");

    public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span) =>
        new List<ClassificationSpan>
        {
            new(new SnapshotSpan(span.Snapshot, span.Span), _type),
        };

    public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
}

The format definition (how the classification looks)

CSHARP
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = "EditorClassifier")]
[Name("EditorClassifier")]
[UserVisible(true)]
internal sealed class EditorClassifierFormat : ClassificationFormatDefinition
{
    public EditorClassifierFormat()
    {
        DisplayName = "EditorClassifier";
        BackgroundColor = Colors.BlueViolet;
        ForegroundColor = Colors.White;
    }
}

Editor extension points at a glance

HookExportReach for it when
ClassifierIClassifierProviderColor/categorize spans of text
TaggerITaggerProviderAttach typed tags (errors, outlining, highlights) to spans
AdornmentAdornmentLayerDefinition + IWpfTextViewCreationListenerDraw WPF visuals over/under text
CompletionIAsyncCompletionSourceProviderCustom IntelliSense
MarginIWpfTextViewMarginProviderAdd a gutter/margin UI strip

MEF components are in-process and lazy — Visual Studio only constructs them when a matching ContentType view opens. Keep constructors cheap; do real work on first use.


Driving Visual Studio from Claude Code

Claude Code runs on the CLI, so orchestrate the build tools, not the GUI. Always pass arguments as arrays (execFileSync) — never interpolate paths into a shell string.

You're the codegen-plus-orchestration layer here — here's how a run flows end to end:

Bash
# Build a solution (prefer the dotnet CLI for SDK-style projects)
dotnet build MyExtension.sln -c Release

# MSBuild for classic VSSDK projects that aren't SDK-style
msbuild MyExtension.sln /p:Configuration=Release /p:DeployExtension=false

# Run tests
dotnet test --logger "trx;LogFileName=results.trx"

# Locate the active VS install (avoids hard-coded paths)
vswhere -latest -property installationPath
JavaScript
// scripts/build-vsix.js — safe argument passing
import { execFileSync } from "node:child_process";

execFileSync("dotnet", ["build", "MyExtension.sln", "-c", "Release"], {
  stdio: "inherit",
});

CI — build the VSIX

The tables above point to "CI build of the .vsix" but never show the workflow. Here it is. Two facts shape it:

  • msbuild, not dotnet build — a classic VSSDK solution that contains a VSIX project won't build with dotnet build even when the projects are SDK-style. Use msbuild directly. (Modern VisualStudio.Extensibility SDK projects can use dotnet build, but msbuild builds both worlds, so the workflow below is the safe default.)
  • DeployExtension=false — on a headless runner there's no local Visual Studio to install into, so suppress the deploy step.

The runner must locate msbuild first. The canonical action is microsoft/setup-msbuild, which runs vswhere and prepends the discovered MSBuild to PATH.

Workflow — .github/workflows/build-vsix.yml

YAML
name: Build VSIX

on:
  push:
    branches: [master]
  pull_request:

jobs:
  build:
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v4

      # Discovers MSBuild via vswhere and adds it to PATH.
      # vs-version pins the toolset; msbuild-architecture defaults to x86.
      - name: Add MSBuild to PATH
        uses: microsoft/setup-msbuild@v3
        with:
          vs-version: '17.0'

      # Restore NuGet packages — msbuild -t:Restore avoids a separate nuget.exe.
      - name: Restore
        run: msbuild MyExtension.sln -t:Restore -p:Configuration=Release

      # Build and pack. DeployExtension=false: no local VS to install into.
      - name: Build VSIX
        run: >-
          msbuild MyExtension.sln
          -p:Configuration=Release
          -p:DeployExtension=false
          -m

      - name: Upload VSIX
        uses: actions/upload-artifact@v4
        with:
          name: MyExtension-vsix
          path: '**/bin/Release/**/*.vsix'
          if-no-files-found: error

Gotcha — the VS extension build tooling may be missing. The windows-latest image ships Visual Studio Build Tools and the .NET workloads, but a classic VSSDK build also needs the Visual Studio extension development workload (the Microsoft.VsSDK.targets that pack the .vsix). On the GitHub-hosted Windows image the full Visual Studio install includes it, but if you hit error MSB4019: The imported project "...Microsoft.VsSDK.targets" was not found, the SDK targets aren't on the runner. Fixes, cheapest first: reference the Microsoft.VSSDK.BuildTools NuGet package so the targets restore with the project (preferred — keeps the build self-contained); or, for a container/self-hosted runner, add the component via the VS Installer (--add Microsoft.VisualStudio.Workload.VisualStudioExtension). The modern VisualStudio.Extensibility SDK sidesteps this entirely — its .Build package brings the packaging targets in as a normal PackageReference.


Common Use Cases

Use CaseApproach
Insert/transform text at caretNew SDK Command + Editor().EditAsync
Syntax highlighting for a custom languageVSSDK IClassifierProvider + ClassificationFormatDefinition
Squiggles / error tagsVSSDK ITaggerProvider<IErrorTag>
Inline visuals (CodeLens-like)VSSDK adornment layer + IWpfTextViewCreationListener
Custom IntelliSenseIAsyncCompletionSourceProvider
Language server integrationNew SDK LSP extension contribution
CI build of the .vsixdotnet build / msbuild in GitHub Actions (Windows runner)

Troubleshooting

IssueFix
Extension not loadingCheck the Experimental Instance: devenv /rootSuffix Exp; reset with /resetSettings
MEF component never constructedContentType mismatch — verify the [ContentType] matches the open file's type
.vsix not producedEnsure Microsoft.VisualStudio.Extensibility.Build (or the VSSDK targets) is referenced
Stale extension after rebuildClear %LocalAppData%\Microsoft\VisualStudio\17.0_*Exp\Extensions cache
msbuild not found in CIUse the microsoft/setup-msbuild action or build with dotnet for SDK-style projects

codeAmani notes

  • Secrets: An IDE extension runs on the developer's machine. Never embed API keys (Anthropic, Daraja, Supabase) in extension assemblies — read them from .env.local / the OS credential store at runtime. A shipped .vsix is trivially decompiled.
  • AI routing: Per the routing policy, an extension that calls an LLM should hit Anthropic Claude for reasoning/codegen and OpenAI for structured/function-calling — but proxy through a server-side endpoint, not a key baked into the VSIX.
  • Where this fits: Visual Studio is a Windows-first .NET tool. For the codeAmani Next.js/M-Pesa stack the day-to-day editor is VS Code; reach for full Visual Studio when working on .NET/C++ backends, MAUI mobile, or building internal tooling extensions. Most contributors on low-bandwidth African connections will favour VS Code's lighter footprint — document any Visual Studio-only workflow so it isn't a hidden prerequisite.
  • Shell safety: When a hook or script invokes dotnet/msbuild/devenv, use execFileSync(cmd, [args]) with array arguments, consistent with the project security convention.