| name | intercepting-commands |
| description | Intercept existing Visual Studio commands to run custom logic before, after, or instead of the default handler. Use when the user asks how to intercept a command, hook into an existing command, override a built-in VS command, run code before/after Save or Build, listen for command execution, or wrap existing command behavior. Covers VisualStudio.Extensibility (out-of-process), VSIX Community Toolkit (in-process), and legacy VSSDK (in-process) approaches. |
Intercepting Existing Commands in Visual Studio Extensions
Command interception lets you run custom logic before, after, or instead of a built-in or third-party Visual Studio command. Common scenarios:
- Run validation before a file is saved
- Log or instrument when the user builds the solution
- Replace the default formatting behavior with a custom formatter
- Show a confirmation prompt before a destructive action
Interception is powerful but invasive — you're inserting your code into Visual Studio's own command pipeline. A slow or buggy interceptor blocks every invocation of that command for the entire IDE. In many cases, listening to events (e.g., document-saved events instead of intercepting the Save command) is less invasive and achieves the same result. Use interception only when you need to run logic before the command executes, or when you need to cancel or replace the default behavior entirely.
When to use this vs. alternatives:
1. VisualStudio.Extensibility (out-of-process, recommended)
The VisualStudio.Extensibility SDK does not currently support intercepting existing commands. You can only define new commands with Command. There is no equivalent of IOleCommandTarget chaining or VS.Commands.InterceptAsync in the out-of-process model.
If you need to intercept existing commands, use the VSIX Community Toolkit or VSSDK (in-process) approach.
For some scenarios you may be able to achieve similar results by listening to events instead of intercepting the command directly. For example, use IDocumentEventsListener to react to document save events rather than intercepting the Save command.
2. VSIX Community Toolkit (in-process)
The Community Toolkit provides VS.Commands.InterceptAsync — a one-line API to intercept any known command by its GUID and ID. You supply callbacks for before and/or after the command executes.
NuGet package: Community.VisualStudio.Toolkit
Key namespace: Community.VisualStudio.Toolkit
Basic interception — run code before and after a command
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
namespace MyExtension;
public sealed class MyExtensionPackage : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
await VS.Commands.InterceptAsync(VSConstants.VSStd97CmdID.Save, ExecuteBeforeSave, ExecuteAfterSave);
}
private CommandProgression ExecuteBeforeSave()
{
return CommandProgression.Continue;
}
private void ExecuteAfterSave()
{
}
}
Cancel a command with CommandProgression.Stop
await VS.Commands.InterceptAsync(VSConstants.VSStd97CmdID.SaveAll, () =>
{
var result = VS.MessageBox.Show(
"Save All",
"Are you sure you want to save all files?",
OLEMSGICON.OLEMSGICON_QUERY,
OLEMSGBUTTON.OLEMSGBUTTON_YESNO);
return result == VSConstants.MessageBoxResult.IDYES
? CommandProgression.Continue
: CommandProgression.Stop;
});
Intercept with only a before-handler
Pass the before-handler only — no after-handler:
await VS.Commands.InterceptAsync(VSConstants.VSStd97CmdID.Build, () =>
{
ThreadHelper.JoinableTaskFactory.Run(async delegate
{
OutputWindowPane pane = await VS.Windows.GetOutputWindowPaneAsync(Windows.VSOutputWindowPane.General);
pane?.WriteLine($"Build started at {DateTime.Now}");
});
return CommandProgression.Continue;
});
Intercept with only an after-handler
Pass null for the before-handler:
await VS.Commands.InterceptAsync(VSConstants.VSStd97CmdID.Build, null, () =>
{
VS.StatusBar.ShowMessageAsync("Build completed!").FireAndForget();
});
Common commands to intercept
Commands are identified by their command group GUID and command ID. The most common are in VSConstants.VSStd97CmdID and VSConstants.VSStd2KCmdID:
| Command | Constant | Description |
|---|
| Save | VSConstants.VSStd97CmdID.Save | Ctrl+S |
| Save All | VSConstants.VSStd97CmdID.SaveAll | Ctrl+Shift+S |
| Build | VSConstants.VSStd97CmdID.Build | Build Solution |
| Rebuild | VSConstants.VSStd97CmdID.Rebuild | Rebuild Solution |
| Cut | VSConstants.VSStd97CmdID.Cut | Ctrl+X |
| Copy | VSConstants.VSStd97CmdID.Copy | Ctrl+C |
| Paste | VSConstants.VSStd97CmdID.Paste | Ctrl+V |
| Undo | VSConstants.VSStd97CmdID.Undo | Ctrl+Z |
| Redo | VSConstants.VSStd97CmdID.Redo | Ctrl+Y |
| Format Document | VSConstants.VSStd2KCmdID.FORMATDOCUMENT | Format entire document |
| Format Selection | VSConstants.VSStd2KCmdID.FORMATSELECTION | Format selected text |
| Find | VSConstants.VSStd97CmdID.Find | Ctrl+F |
| Replace | VSConstants.VSStd97CmdID.Replace | Ctrl+H |
Intercepting a command by raw GUID and ID
For commands not in VSConstants, use the overload that takes a CommandID:
using System.ComponentModel.Design;
var commandGuid = new Guid("your-command-group-guid");
var commandId = new CommandID(commandGuid, 0x0100);
await VS.Commands.InterceptAsync(commandId, () =>
{
return CommandProgression.Continue;
});
Key points
InterceptAsync must be called after the package is initialized (inside InitializeAsync).
- The before-handler runs synchronously on the UI thread. Keep it fast — offload heavy work to a background thread.
- Return
CommandProgression.Continue to let the original command execute, or CommandProgression.Stop to cancel it.
- The after-handler receives no arguments and no return value.
3. VSSDK (in-process, legacy)
With the raw VSSDK, command interception is done by implementing IOleCommandTarget and inserting your implementation into the command target chain using IVsRegisterPriorityCommandTarget.
NuGet package: Microsoft.VisualStudio.SDK
Key namespaces: Microsoft.VisualStudio.Shell, Microsoft.VisualStudio.OLE.Interop, Microsoft.VisualStudio.Shell.Interop
Step 1: Implement IOleCommandTarget
using System;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
namespace MyExtension;
internal sealed class SaveCommandInterceptor : IOleCommandTarget
{
private readonly AsyncPackage _package;
public SaveCommandInterceptor(AsyncPackage package)
{
_package = package;
}
public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
{
return (int)Constants.OLECMDERR_E_UNKNOWNGROUP;
}
public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
{
ThreadHelper.ThrowIfNotOnUIThread();
if (pguidCmdGroup == typeof(VSConstants.VSStd97CmdID).GUID
&& nCmdID == (uint)VSConstants.VSStd97CmdID.Save)
{
var result = VsShellUtilities.ShowMessageBox(
_package,
"Save this file?",
"Confirm Save",
OLEMSGICON.OLEMSGICON_QUERY,
OLEMSGBUTTON.OLEMSGBUTTON_YESNO,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
if (result != (int)VSConstants.MessageBoxResult.IDYES)
{
return VSConstants.S_OK;
}
}
return (int)Constants.OLECMDERR_E_UNKNOWNGROUP;
}
}
Step 2: Register the interceptor in the package
Use IVsRegisterPriorityCommandTarget to insert your IOleCommandTarget at a higher priority than the default handlers:
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[Guid("YOUR-PACKAGE-GUID")]
public sealed class MyExtensionPackage : AsyncPackage
{
private uint _interceptorCookie;
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var priorityCommandTarget = await GetServiceAsync(typeof(SVsRegisterPriorityCommandTarget))
as IVsRegisterPriorityCommandTarget;
var interceptor = new SaveCommandInterceptor(this);
priorityCommandTarget?.RegisterPriorityCommandTarget(
0,
interceptor,
out _interceptorCookie);
}
protected override void Dispose(bool disposing)
{
if (disposing && _interceptorCookie != 0)
{
ThreadHelper.ThrowIfNotOnUIThread();
var priorityCommandTarget = GetService(typeof(SVsRegisterPriorityCommandTarget))
as IVsRegisterPriorityCommandTarget;
priorityCommandTarget?.UnregisterPriorityCommandTarget(_interceptorCookie);
_interceptorCookie = 0;
}
base.Dispose(disposing);
}
}
Intercepting for after-execution logic
IOleCommandTarget.Exec intercepts before the default handler. To run logic after the command executes, you need to let the default handler run first by calling the next target in the chain. The simplest way is to use IVsUIShell.PostExecCommand to queue your post-execution logic, or track state and use IOleCommandTarget chaining.
A simpler approach for after-execution scenarios: use DTE.Events.CommandEvents to listen for command completion:
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
var dte = await GetServiceAsync(typeof(DTE)) as DTE2;
var commandEvents = dte.Events.CommandEvents;
_commandEvents = commandEvents;
commandEvents.AfterExecute += (string guid, int id, object customIn, object customOut) =>
{
if (guid == typeof(VSConstants.VSStd97CmdID).GUID.ToString("B")
&& id == (int)VSConstants.VSStd97CmdID.Save)
{
}
};
Important: Store a reference to the CommandEvents object in a field. If it gets garbage collected, the event handler silently stops firing.
Key guidance
- VisualStudio.Extensibility does not support command interception. Use event listeners (e.g.,
IDocumentEventsListener) as an alternative for some scenarios.
- Community Toolkit —
VS.Commands.InterceptAsync is the simplest approach. Use CommandProgression.Stop to cancel, CommandProgression.Continue to allow. Call it in InitializeAsync.
- VSSDK — Implement
IOleCommandTarget and register it via IVsRegisterPriorityCommandTarget. Always unregister in Dispose. For after-execution logic, use DTE.Events.CommandEvents.AfterExecute.
- Keep before-handlers fast — they run on the UI thread.
- Always handle the case where the user cancels (return appropriate results).
- For file save scenarios, consider whether events (
RunningDocumentTable, IDocumentEventsListener) are more appropriate than command interception.
Troubleshooting
- Interceptor never fires: For the Toolkit, verify
VS.Commands.InterceptAsync is called in InitializeAsync with the correct command GUID and ID. For VSSDK, ensure you called RegisterPriorityCommandTarget and that your IOleCommandTarget.QueryStatus returns OLECMDF.OLECMDF_SUPPORTED | OLECMDF.OLECMDF_ENABLED.
- Interceptor fires but the original command still runs: For the Toolkit, return
CommandProgression.Stop from your handler to prevent the original command from executing. For VSSDK, return S_OK from Exec without calling the next command target.
- IDE becomes sluggish after adding interceptor: Your interceptor is doing too much work on the UI thread. Keep before-handlers fast and offload work to a background thread.
- After-execution logic doesn't see updated state: For VSSDK
CommandEvents.AfterExecute, the event fires synchronously after the command returns. If the command's effects are applied asynchronously, your handler may run before they're visible. Use await Task.Yield() or a small delay to let the effects settle.
UnregisterPriorityCommandTarget throws: You're calling it from a background thread or after the VS shell has already been disposed. Wrap in ThreadHelper.ThrowIfNotOnUIThread() and check disposal state.
What NOT to do
Do NOT intercept commands when event listeners suffice — e.g., IDocumentEventsListener for post-save is less fragile than intercepting Save. Reserve interception for canceling or replacing behavior.
Do NOT do slow work in a before-execution interceptor — it runs synchronously on the UI thread for every invocation.
Do NOT forget to unregister priority command targets in Dispose — leaked registrations cause crashes on disposed objects.
Do NOT intercept commands in VisualStudio.Extensibility — not supported. Use event listeners (e.g., IDocumentEventsListener) instead.
See also
References