Adds commands (menu items, toolbar buttons) to Visual Studio extensions using VisualStudio.Extensibility, VSIX Community Toolkit, or legacy VSSDK. Covers .vsct files, command handlers, and icon placement.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:adding-commandsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A command is any user-invokable action — menu items, toolbar buttons, context menu entries, keyboard shortcuts. Each command has an ID, a display name, an optional icon, a placement (where it appears in the UI), and an execution handler.
A command is any user-invokable action — menu items, toolbar buttons, context menu entries, keyboard shortcuts. Each command has an ID, a display name, an optional icon, a placement (where it appears in the UI), and an execution handler.
Commands are the primary way users interact with an extension — without one, your extension has no entry point.
When to use this vs. alternatives:
Every command class should be in its own .cs file inside a top-level Commands/ folder in the project:
MyExtension/
├── Commands/
│ ├── BuildSolutionCommand.cs
│ ├── FormatDocumentCommand.cs
│ └── OpenSettingsCommand.cs
├── MyExtensionPackage.cs ← (Toolkit / VSSDK only)
├── MyExtension.csproj
└── ...
One class per file. Name the file to match the class name.
Commands are self-contained classes that extend Command. No .vsct file is needed — placement, icon, and metadata are declared in code via CommandConfiguration.
NuGet package: Microsoft.VisualStudio.Extensibility
Key namespaces: Microsoft.VisualStudio.Extensibility, Microsoft.VisualStudio.Extensibility.Commands
Commands/BuildSolutionCommand.cs:
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Commands;
namespace MyExtension.Commands;
[VisualStudioContribution]
internal class BuildSolutionCommand : Command
{
public BuildSolutionCommand(VisualStudioExtensibility extensibility)
: base(extensibility) { }
public override CommandConfiguration CommandConfiguration => new("Build Solution")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
Icon = new(ImageMoniker.KnownValues.BuildSolution, IconSettings.IconAndText),
};
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken ct)
{
// Command logic here
await this.Extensibility.Shell().ShowPromptAsync(
"Build started.",
PromptOptions.OK,
ct);
}
}
Use CommandPlacement.KnownPlacements for standard locations:
| Placement | Menu location |
|---|---|
ToolsMenu | Tools menu |
ViewOtherWindowsMenu | View > Other Windows |
ExtensionsMenu | Extensions menu |
For custom parent groups, use CommandPlacement.VsctParent with the GUID and ID of the parent group from the .vsct schema.
Use ImageMoniker.KnownValues for built-in VS icons:
Icon = new(ImageMoniker.KnownValues.BuildSolution, IconSettings.IconAndText),
Icon = new(ImageMoniker.KnownValues.Settings, IconSettings.IconOnly),
Icon = new(ImageMoniker.KnownValues.AddFile, IconSettings.IconAndText),
IconSettings:
IconAndText — shows both the icon and the display name.IconOnly — shows only the icon (used in toolbars).Place images as embedded resources and reference by custom moniker:
[VisualStudioContribution]
internal static ImageMoniker MyCustomIcon = new("MyCustomIcon", KnownImageIds.ImageCatalogGuid);
public override CommandConfiguration CommandConfiguration => new("Format Document")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
Shortcuts = [new CommandShortcutConfiguration(ModifierKey.Control, Key.K, ModifierKey.Control, Key.D)],
};
Show a command only when specific conditions are met:
public override CommandConfiguration CommandConfiguration => new("Analyze C# File")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
Icon = new(ImageMoniker.KnownValues.CSFileNode, IconSettings.IconAndText),
VisibleWhen = ActivationConstraint.ClientContext(
ClientContextKey.Shell.ActiveSelectionFileName, @"\.cs$"),
};
Override CommandConfiguration.EnabledWhen or toggle state in ExecuteCommandAsync:
public override CommandConfiguration CommandConfiguration => new("Debug Command")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
EnabledWhen = ActivationConstraint.SolutionState(SolutionState.FullyLoaded),
};
Commands extend BaseCommand<T>. Each command is its own class and file. Commands must be declared in a .vsct file, which defines the button, its parent group, its icon, and keyboard shortcut.
NuGet package: Community.VisualStudio.Toolkit
Key namespace: Community.VisualStudio.Toolkit
The .vsct file declares the command table — symbols, groups, buttons, and their placements. Every command button needs an entry here.
MyExtensionPackage.vsct:
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Include href="KnownImageIds.vsct"/>
<Commands package="guidMyExtensionPackage">
<!-- Define a command group -->
<Groups>
<Group guid="guidMyExtensionCmdSet" id="MyMenuGroup" priority="0x0600">
<!-- Parent the group under the Tools menu -->
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
<!-- Define the command buttons -->
<Buttons>
<Button guid="guidMyExtensionCmdSet" id="BuildSolutionCommandId" priority="0x0100" type="Button">
<Parent guid="guidMyExtensionCmdSet" id="MyMenuGroup"/>
<Icon guid="ImageCatalogGuid" id="BuildSolution"/>
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>Build Solution</ButtonText>
</Strings>
</Button>
<Button guid="guidMyExtensionCmdSet" id="FormatDocumentCommandId" priority="0x0200" type="Button">
<Parent guid="guidMyExtensionCmdSet" id="MyMenuGroup"/>
<Icon guid="ImageCatalogGuid" id="FormatDocument"/>
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>Format Document</ButtonText>
</Strings>
</Button>
<Button guid="guidMyExtensionCmdSet" id="OpenSettingsCommandId" priority="0x0300" type="Button">
<Parent guid="guidMyExtensionCmdSet" id="MyMenuGroup"/>
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>Open Settings</ButtonText>
</Strings>
</Button>
</Buttons>
</Commands>
<!-- Symbol definitions -->
<Symbols>
<GuidSymbol name="guidMyExtensionPackage" value="{YOUR-PACKAGE-GUID}" />
<GuidSymbol name="guidMyExtensionCmdSet" value="{YOUR-CMDSET-GUID}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="BuildSolutionCommandId" value="0x0100" />
<IDSymbol name="FormatDocumentCommandId" value="0x0101" />
<IDSymbol name="OpenSettingsCommandId" value="0x0102" />
</GuidSymbol>
</Symbols>
</CommandTable>
<Include href="KnownImageIds.vsct"/> at the top of the <CommandTable>.<Icon> element, use guid="ImageCatalogGuid" and set id to the KnownMoniker name (e.g., BuildSolution, Settings, FormatDocument).IconIsMoniker flag: Inside each <Button>, add <CommandFlag>IconIsMoniker</CommandFlag>. Without this flag, VS treats the icon reference as a legacy bitmap strip and the icon won't display.| KnownMoniker name | Use case |
|---|---|
BuildSolution | Build / compile actions |
FormatDocument | Formatting commands |
Settings | Settings / configuration |
AddFile | Add or create file |
Delete | Remove / delete |
Refresh | Refresh / reload |
Search | Search / find |
Save | Save operations |
RunOutline | Run / execute |
CSFileNode | C# file operations |
StatusInformation | Information / about |
StatusWarning | Warnings |
StatusError | Error indicators |
To browse the full list of 3,800+ KnownMonikers, install the KnownMonikers Explorer extension from the Visual Studio Marketplace. It adds a tool window (View > Other Windows > KnownMonikers Explorer) that lets you search and preview all available monikers.
<KeyBindings>
<KeyBinding guid="guidMyExtensionCmdSet" id="FormatDocumentCommandId"
editor="guidVSStd97"
key1="K" mod1="Control"
key2="D" mod2="Control" />
</KeyBindings>
Place the <KeyBindings> block as a sibling of <Commands> inside <CommandTable>.
Change the <Parent> on the <Group> to target a different menu:
| Menu | guid | id |
|---|---|---|
| Tools | guidSHLMainMenu | IDM_VS_MENU_TOOLS |
| Edit | guidSHLMainMenu | IDM_VS_MENU_EDIT |
| View | guidSHLMainMenu | IDM_VS_MENU_VIEW |
| Extensions | guidSHLMainMenu | IDM_VS_MENU_EXTENSIONS |
| Solution Explorer context menu | guidSHLMainMenu | IDM_VS_CTXT_SOLNNODE |
| Code window context menu | guidSHLMainMenu | IDM_VS_CTXT_CODEWIN |
| Project context menu | guidSHLMainMenu | IDM_VS_CTXT_PROJNODE |
Each command gets its own file in the Commands/ folder.
Commands/BuildSolutionCommand.cs:
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
[Command(PackageIds.BuildSolutionCommandId)]
internal sealed class BuildSolutionCommand : BaseCommand<BuildSolutionCommand>
{
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
await VS.MessageBox.ShowAsync("Build Solution", "Starting build...");
}
}
Commands/FormatDocumentCommand.cs:
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
[Command(PackageIds.FormatDocumentCommandId)]
internal sealed class FormatDocumentCommand : BaseCommand<FormatDocumentCommand>
{
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
await VS.MessageBox.ShowAsync("Format Document", "Formatting...");
}
}
Commands/OpenSettingsCommand.cs:
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
[Command(PackageIds.OpenSettingsCommandId)]
internal sealed class OpenSettingsCommand : BaseCommand<OpenSettingsCommand>
{
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
await VS.MessageBox.ShowAsync("Settings", "Opening settings...");
}
}
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageGuids.MyExtensionPackageString)]
public sealed class MyExtensionPackage : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
}
}
RegisterCommandsAsync() discovers all BaseCommand<T> subclasses in the assembly and registers them automatically.
Override BeforeQueryStatus to toggle the enabled or visible state:
protected override void BeforeQueryStatus(EventArgs e)
{
Command.Enabled = /* your condition */;
Command.Visible = /* your condition */;
}
Commands are registered manually with OleMenuCommandService. Like the Toolkit, commands must be declared in a .vsct file (see the .vsct example in section 2 — the file format is identical).
NuGet package: Microsoft.VisualStudio.SDK
Key namespaces: Microsoft.VisualStudio.Shell, Microsoft.VisualStudio.Shell.Interop
Use the same .vsct structure shown in section 2. The button definitions, <Include href="KnownImageIds.vsct"/>, guid="ImageCatalogGuid", and <CommandFlag>IconIsMoniker</CommandFlag> rules are identical.
Each command is a standalone class with a static Initialize method and a private constructor. Keep each in its own file under Commands/.
Commands/BuildSolutionCommand.cs:
using System;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
internal sealed class BuildSolutionCommand
{
public static readonly Guid CommandSet = new("YOUR-CMDSET-GUID");
public const int CommandId = 0x0100;
private readonly AsyncPackage _package;
private BuildSolutionCommand(AsyncPackage package, OleMenuCommandService commandService)
{
_package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandId = new CommandID(CommandSet, CommandId);
var menuItem = new MenuCommand(Execute, menuCommandId);
commandService.AddCommand(menuItem);
}
public static async Task InitializeAsync(AsyncPackage package)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
_ = new BuildSolutionCommand(package, commandService);
}
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
VsShellUtilities.ShowMessageBox(
_package,
"Starting build...",
"Build Solution",
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
}
Commands/FormatDocumentCommand.cs:
using System;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
internal sealed class FormatDocumentCommand
{
public static readonly Guid CommandSet = new("YOUR-CMDSET-GUID");
public const int CommandId = 0x0101;
private readonly AsyncPackage _package;
private FormatDocumentCommand(AsyncPackage package, OleMenuCommandService commandService)
{
_package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandId = new CommandID(CommandSet, CommandId);
var menuItem = new MenuCommand(Execute, menuCommandId);
commandService.AddCommand(menuItem);
}
public static async Task InitializeAsync(AsyncPackage package)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
_ = new FormatDocumentCommand(package, commandService);
}
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
VsShellUtilities.ShowMessageBox(
_package,
"Formatting document...",
"Format Document",
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
}
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration("#110", "#112", "1.0")]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid("YOUR-PACKAGE-GUID")]
public sealed class MyExtensionPackage : AsyncPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await BuildSolutionCommand.InitializeAsync(this);
await FormatDocumentCommand.InitializeAsync(this);
await OpenSettingsCommand.InitializeAsync(this);
}
}
Each command must be initialized explicitly — there is no auto-discovery in raw VSSDK.
Use OleMenuCommand instead of MenuCommand to get BeforeQueryStatus:
var menuCommandId = new CommandID(CommandSet, CommandId);
var menuItem = new OleMenuCommand(Execute, menuCommandId);
menuItem.BeforeQueryStatus += (s, e) =>
{
ThreadHelper.ThrowIfNotOnUIThread();
menuItem.Enabled = /* your condition */;
menuItem.Visible = /* your condition */;
};
commandService.AddCommand(menuItem);
Commands/ folder. Name the file after the class..vsct file needed. Placement, icons, and shortcuts are declared in CommandConfiguration. Use ImageMoniker.KnownValues for icons..vsct file. To use KnownMonikers as icons: (1) add <Include href="KnownImageIds.vsct"/>, (2) set guid="ImageCatalogGuid" on <Icon>, and (3) add <CommandFlag>IconIsMoniker</CommandFlag> to the button.[ProvideMenuResource("Menus.ctmenu", 1)] is required on the package for Toolkit and VSSDK — without it, VS won't load the command table.AsyncPackage with AllowsBackgroundLoading = true for VSSDK. The Toolkit uses ToolkitPackage which already handles this.Execute. Offload to a background thread and switch back with JoinableTaskFactory.SwitchToMainThreadAsync() only when needed.[VisualStudioContribution] attribute is on the command class and that Placements is set in CommandConfiguration. For Toolkit/VSSDK, verify [ProvideMenuResource("Menus.ctmenu", 1)] is on the package, the .vsct button definition exists, and the GUID/ID symbols match between .vsct and code..vsct button: <Include href="KnownImageIds.vsct"/> at the top of the command table, guid="ImageCatalogGuid" on the <Icon> element, and <CommandFlag>IconIsMoniker</CommandFlag> on the button. Missing any one of these causes a blank icon with no error.EnabledWhen constraints in CommandConfiguration. For Toolkit/VSSDK, check your BeforeQueryStatus handler — it may be setting Enabled = false unconditionally.CommandID. Check for duplicate IDSymbol values in your .vsct file.[Command(PackageIds.XYZ)] attribute references the correct generated constant. For VSSDK, confirm InitializeAsync calls your command's InitializeAsync and that the CommandID matches the .vsct symbol.Shortcuts array in CommandConfiguration. For .vsct, ensure the <KeyBinding> element uses editor="guidVSStd97" for global scope and that the key combo doesn't conflict with an existing VS binding.Do NOT do heavy work in
Execute/ExecuteCommandAsyncon the UI thread — offload withTask.Runorawait TaskScheduler.Default. See vs-async-threading.
Do NOT forget
[ProvideMenuResource("Menus.ctmenu", 1)]on the package class (Toolkit/VSSDK) — without it, all buttons silently fail to appear.
Do NOT reuse GUID + ID pairs across commands — duplicates cause a startup exception. Each command needs a unique
CommandID.
Do NOT hard-code command GUIDs/IDs as inline strings. Use constants in a shared class or Toolkit's generated
PackageIds/PackageGuids.
Do NOT use synchronous
Package— useAsyncPackage(VSSDK) orToolkitPackage(Toolkit). Sync packages degrade IDE launch time.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsControls command visibility, enablement, and hiding in Visual Studio extensions using declarative constraints and dynamic BeforeQueryStatus callbacks.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.