Visual Studio Integration Guide
What is Visual Studio?
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.
██╗ ██╗██╗███████╗██╗ ██╗ █████╗ ██╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
██║ ██║██║██╔════╝██║ ██║██╔══██╗██║ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
██║ ██║██║███████╗██║ ██║███████║██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
╚██╗ ██╔╝██║╚════██║██║ ██║██╔══██║██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
╚████╔╝ ██║███████║╚██████╔╝██║ ██║███████╗ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
╚═══╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝Visual Studio Integration Guide
Focus: Extending the Visual Studio IDE's editor and tooling — building VSIX extensions with the modern out-of-process
VisualStudio.ExtensibilitySDK and the classic VSSDK + MEF editor model (classifiers, adornments, taggers), and driving builds/tests from Claude Code via MSBuild and thedotnet/devenvCLIs.
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:
| Model | Process | API style | Use it when |
|---|---|---|---|
| VisualStudio.Extensibility SDK (new) | Out-of-process | Async, [VisualStudioContribution], hot-reload | New commands, editor listeners, tool windows, LSP — start here |
| VSSDK + MEF (classic) | In-process (COM) | [Export]/[Import], requires VS restart | Deep 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
| Resource | URL |
|---|---|
| Extensibility overview | https://learn.microsoft.com/en-us/visualstudio/extensibility/ |
| VisualStudio.Extensibility (new SDK) | https://learn.microsoft.com/en-us/visualstudio/extensibility/visualstudio.extensibility/ |
| Editor extensibility points | https://learn.microsoft.com/en-us/visualstudio/extensibility/extensibility-points-of-the-editor |
| VSExtensibility SDK (GitHub + samples) | https://github.com/microsoft/VSExtensibility |
| Visual Studio IDE docs | https://learn.microsoft.com/en-us/visualstudio/ide/ |
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:
| Package | Purpose | Version at review |
|---|---|---|
Microsoft.VisualStudio.Extensibility.Sdk | Core out-of-process SDK | 17.14.40608 |
Microsoft.VisualStudio.Extensibility.Build | MSBuild targets that pack the .vsix | 17.14.40608 |
Microsoft.VisualStudio.SDK (classic) | Meta-package for VSSDK/MEF editor APIs | matches your VS version (e.g. 17.x) |
Latest versions: https://www.nuget.org/packages/Microsoft.VisualStudio.Extensibility.Sdk
Setup — Modern SDK (recommended)
Prerequisites
Visual Studio 2022 17.9+ with the "Visual Studio extension development" workload
.NET 8 SDKProject 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:
<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:
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:
[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
# 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.vsixSetup — 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)
[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)
[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
| Hook | Export | Reach for it when |
|---|---|---|
| Classifier | IClassifierProvider | Color/categorize spans of text |
| Tagger | ITaggerProvider | Attach typed tags (errors, outlining, highlights) to spans |
| Adornment | AdornmentLayerDefinition + IWpfTextViewCreationListener | Draw WPF visuals over/under text |
| Completion | IAsyncCompletionSourceProvider | Custom IntelliSense |
| Margin | IWpfTextViewMarginProvider | Add a gutter/margin UI strip |
MEF components are in-process and lazy — Visual Studio only constructs them when a matching
ContentTypeview 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:
# 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// 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, notdotnet build— a classic VSSDK solution that contains a VSIX project won't build withdotnet buildeven when the projects are SDK-style. Usemsbuilddirectly. (ModernVisualStudio.ExtensibilitySDK projects can usedotnet build, butmsbuildbuilds 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
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: errorGotcha — the VS extension build tooling may be missing. The
windows-latestimage ships Visual Studio Build Tools and the .NET workloads, but a classic VSSDK build also needs the Visual Studio extension development workload (theMicrosoft.VsSDK.targetsthat pack the.vsix). On the GitHub-hosted Windows image the full Visual Studio install includes it, but if you hiterror MSB4019: The imported project "...Microsoft.VsSDK.targets" was not found, the SDK targets aren't on the runner. Fixes, cheapest first: reference theMicrosoft.VSSDK.BuildToolsNuGet 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 modernVisualStudio.ExtensibilitySDK sidesteps this entirely — its.Buildpackage brings the packaging targets in as a normalPackageReference.
Common Use Cases
| Use Case | Approach |
|---|---|
| Insert/transform text at caret | New SDK Command + Editor().EditAsync |
| Syntax highlighting for a custom language | VSSDK IClassifierProvider + ClassificationFormatDefinition |
| Squiggles / error tags | VSSDK ITaggerProvider<IErrorTag> |
| Inline visuals (CodeLens-like) | VSSDK adornment layer + IWpfTextViewCreationListener |
| Custom IntelliSense | IAsyncCompletionSourceProvider |
| Language server integration | New SDK LSP extension contribution |
CI build of the .vsix | dotnet build / msbuild in GitHub Actions (Windows runner) |
Troubleshooting
| Issue | Fix |
|---|---|
| Extension not loading | Check the Experimental Instance: devenv /rootSuffix Exp; reset with /resetSettings |
| MEF component never constructed | ContentType mismatch — verify the [ContentType] matches the open file's type |
.vsix not produced | Ensure Microsoft.VisualStudio.Extensibility.Build (or the VSSDK targets) is referenced |
| Stale extension after rebuild | Clear %LocalAppData%\Microsoft\VisualStudio\17.0_*Exp\Extensions cache |
msbuild not found in CI | Use 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.vsixis 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, useexecFileSync(cmd, [args])with array arguments, consistent with the project security convention.