VS Code Extension Patterns Skill
Reusable patterns for VS Code extension development.
⚠️ Staleness Warning
VS Code APIs evolve with each monthly release. Patterns may become outdated or better alternatives may emerge.
Refresh triggers:
- •VS Code major/minor releases
- •New proposed APIs becoming stable
- •Extension API deprecations
- •Webview security policy changes
Last validated: February 2026 (VS Code 1.109+)
Check current state: VS Code API, Release Notes
Webview Dashboard
// Gather data in parallel, build HTML with async
const [health, knowledge, sync] = await Promise.all([
checkHealth(true), getKnowledgeSummary(), getSyncStatus()
]);
panel.webview.html = await getWebviewContent(health, knowledge, sync);
Key: Make getWebviewContent async if it needs directory scanning or other async ops.
TreeDataProvider for Sidebar
class WelcomeViewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView: vscode.WebviewView) {
webviewView.webview.options = { enableScripts: true };
webviewView.webview.html = this.getHtmlContent();
webviewView.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'refresh': await this.refresh(); break;
}
});
}
}
// Register in extension.ts
vscode.window.registerWebviewViewProvider('alex.welcomeView', new WelcomeViewProvider());
CSP-Compliant Webview Event Handling
Problem: Inline event handlers (onclick="...") violate Content Security Policy and can be blocked.
Solution: Use data-cmd attributes with delegated event listeners:
<!-- ❌ WRONG: Inline handlers (CSP violation) --> <button onclick="handleClick()">Click</button> <!-- ✅ CORRECT: data-cmd pattern --> <button data-cmd="play">Play</button> <button data-cmd="stop">Stop</button>
// Single delegated listener for all commands
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-cmd]');
if (!target) return;
const cmd = target.getAttribute('data-cmd');
switch (cmd) {
case 'play': audio.play(); break;
case 'stop': audio.pause(); audio.currentTime = 0; break;
}
});
Benefits:
- •CSP-compliant (no inline scripts)
- •Single event listener (better performance)
- •Easy to add new commands
- •Consistent pattern across webviews
Safe Configuration Pattern
Tiered settings: Essential (🔴) → Recommended (🟡) → Nice-to-Have (🟢)
Safety rules:
- •Additive only — never modify/remove existing
- •Check
config.inspect(key)?.globalValuebefore applying - •Preview JSON before changes
- •User chooses categories
async function applySettings(settings: Record<string, unknown>) {
const config = vscode.workspace.getConfiguration();
for (const [key, value] of Object.entries(settings)) {
if (config.inspect(key)?.globalValue === undefined) {
await config.update(key, value, vscode.ConfigurationTarget.Global);
}
}
}
Auto-Detection with Confidence
const PATTERNS = [
{ pattern: /learned|discovered|realized/i, confidence: 0.8 },
{ pattern: /key insight|the trick is/i, confidence: 0.85 },
];
Use confidence thresholds for auto-actions. Higher threshold = fewer false positives.
Duplicate Detection
function isDuplicate(newText: string, existing: string[]): boolean {
const normalize = (s: string) => s.toLowerCase().replace(/[^\w\s]/g, '');
return existing.some(e => calculateSimilarity(normalize(newText), normalize(e)) > 0.8);
}
Portability Rules
Extensions must work on any machine:
// ✅ CORRECT: Dynamic paths const rootPath = vscode.workspace.workspaceFolders?.[0].uri.fsPath; const globalPath = path.join(os.homedir(), '.alex'); // ❌ WRONG: Hardcoded paths const rootPath = 'c:\\Development\\MyProject'; // Never!
Key utilities:
- •
vscode.workspace.workspaceFolders— Current workspace - •
os.homedir()— Platform-independent home - •
path.join()— Cross-platform path building
Publishing Workflow
# Load PAT from .env
$env:VSCE_PAT = (Get-Content .env | Select-String "VSCE_PAT" | ForEach-Object { $_.Line.Split("=",2)[1] })
vsce publish
Version collision: Increment patch → update package.json, README badge, CHANGELOG → retry.
Goals with Streak Tracking
interface LearningGoal {
id: string;
title: string;
category: 'coding' | 'reading' | 'practice' | 'review';
targetCount: number;
currentCount: number;
type: 'daily' | 'weekly';
expiresAt: string;
}
// Auto-increment on activity
async function autoIncrementGoals(activityType: 'session' | 'insight') {
const data = await loadGoalsData();
for (const goal of data.goals) {
if (shouldIncrement(goal, activityType) && !isExpired(goal)) {
goal.currentCount = Math.min(goal.currentCount + 1, goal.targetCount);
}
}
await saveGoalsData(data);
}
SecretStorage for Sensitive Tokens
Never store secrets in settings — use VS Code's SecretStorage API:
// Module-level cache
let secretStorage: vscode.SecretStorage | null = null;
let cachedToken: string | null = null;
// Initialize during activation
export async function initSecrets(context: vscode.ExtensionContext): Promise<void> {
secretStorage = context.secrets;
cachedToken = await secretStorage.get('myExtension.apiToken') || null;
// Migration: Move token from settings to secrets
const config = vscode.workspace.getConfiguration('myExtension');
const settingsToken = config.get<string>('apiToken')?.trim();
if (settingsToken && !cachedToken) {
await secretStorage.store('myExtension.apiToken', settingsToken);
cachedToken = settingsToken;
await config.update('apiToken', undefined, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage('Token migrated to secure storage.');
}
}
// Synchronous access to cached value
function getToken(): string | null {
return cachedToken;
}
Key points:
- •
context.secrets.get()/store()/delete()are async - •Cache at module level for sync access
- •Migrate existing settings tokens on first run
- •Mark old setting as deprecated in package.json
Webview CSP Security
Always add Content-Security-Policy when enableScripts: true:
import { getNonce } from './sanitize';
function getWebviewHtml(webview: vscode.Webview): string {
const nonce = getNonce();
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';
img-src ${webview.cspSource} https: data:;
font-src ${webview.cspSource};
">
</head>
<body>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
// ... your code
</script>
</body>
</html>`;
}
// Nonce generator
function getNonce(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length: 32 }, () =>
chars.charAt(Math.floor(Math.random() * chars.length))
).join('');
}
CSP Event Delegation (onclick → data-cmd)
Problem: After adding CSP with script-src 'nonce-${nonce}', all inline event handlers (onclick, onchange) stop working because CSP blocks inline JavaScript.
Solution: Replace inline handlers with data attributes and event delegation:
<!-- ❌ BLOCKED BY CSP -->
<button onclick="cmd('upgrade')">Upgrade</button>
<button onclick="cmd('launchSkill', {skill: 'code-review'})">Review</button>
<!-- ✅ CSP-COMPLIANT -->
<button data-cmd="upgrade">Upgrade</button>
<button data-cmd="launchSkill" data-skill="code-review">Review</button>
<script nonce="${nonce}">
document.addEventListener('click', function(e) {
const el = e.target.closest('[data-cmd]');
if (el) {
e.preventDefault();
const command = el.getAttribute('data-cmd');
const skill = el.getAttribute('data-skill');
vscode.postMessage(skill ? { command, skill } : { command });
}
});
</script>
Benefits:
- •Security — CSP blocks all inline scripts
- •Performance — Single event listener vs many handlers
- •Maintainability — Commands defined as data, not code
Webview Sandbox: postMessage Required
Problem: window.open(), location.reload(), and direct external links silently fail in sandboxed webviews.
Solution: WebView sends message to extension host; extension performs privileged action:
// In webview HTML
vscode.postMessage({ type: 'openExternal', url: 'https://example.com' });
// In extension host
panel.webview.onDidReceiveMessage(async (message) => {
if (message.type === 'openExternal') {
await vscode.env.openExternal(vscode.Uri.parse(message.url));
}
});
Key insight: WebView ↔ Extension Host communication mirrors browser Content Script ↔ Background Script patterns.
Telemetry Opt-Out Compliance
Always respect VS Code's telemetry settings:
function isTelemetryEnabled(): boolean {
// Check VS Code global setting first
if (!vscode.env.isTelemetryEnabled) {
return false;
}
// Then check extension-specific setting
const config = vscode.workspace.getConfiguration('myExtension');
return config.get<boolean>('telemetry.enabled', true);
}
function log(event: string, data?: Record<string, unknown>): void {
if (!isTelemetryEnabled()) {
return;
}
// Send telemetry...
}
Configuration Change Listeners
React to settings changes at runtime:
export function activate(context: vscode.ExtensionContext) {
// Listen for configuration changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('myExtension.featureA')) {
// Refresh feature A
refreshFeatureA();
}
if (e.affectsConfiguration('myExtension.telemetry')) {
// Update telemetry state
}
})
);
}
Key points:
- •Use
affectsConfiguration()to filter relevant changes - •Push listener to
context.subscriptionsfor cleanup - •Re-read config values, don't cache indefinitely
VS Code 1.109+ Agent Platform Capabilities
VS Code 1.109 introduces a native agent platform that extensions can leverage:
Agent Files (AGENTS.md)
Extensions can ship agent definitions that VS Code auto-discovers:
<!-- .github/agents/my-agent.agent.md --> --- name: "MyAgent" description: "Specialized agent for domain X" --- # MyAgent Instructions Agent-specific instructions and knowledge go here.
Setting: chat.useAgentsMdFile: true enables automatic loading.
Skills Loading
Extensions can define skills in .github/skills/ that are auto-loaded into chat:
Setting: chat.agentSkillsLocations: [".github/skills"]
Each skill folder contains a SKILL.md (knowledge) and optional synapses.json (connections).
Chat Participant API
Register custom chat participants that users can @mention:
const participant = vscode.chat.createChatParticipant('myext.agent', async (request, context, stream, token) => {
// Access to request.prompt, request.command
// Stream responses with stream.markdown(), stream.button(), stream.reference()
stream.markdown('Hello from my agent!');
});
participant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'icon.png');
context.subscriptions.push(participant);
Tool Registration
Register tools that any chat participant can invoke:
const tool = vscode.lm.registerTool('myext-searchDocs', {
async invoke(options, token) {
const query = options.input.query;
// Perform tool action
return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(JSON.stringify(results))
]);
}
});
context.subscriptions.push(tool);
Declare in package.json:
{
"contributes": {
"languageModelTools": [{
"name": "myext-searchDocs",
"displayName": "Search Documentation",
"modelDescription": "Searches project documentation for relevant content",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}
}]
}
}
Extended Thinking
Models supporting extended thinking can be configured per-model:
{
"claude-opus-4-*.extendedThinkingEnabled": true,
"claude-opus-4-*.thinkingBudget": 16384
}
MCP Integration
VS Code 1.109+ supports Model Context Protocol servers:
Setting: chat.mcp.gallery.enabled: true
MCP servers extend AI capabilities with external tools (Azure, GitHub, databases).
Key 1.109 Settings Summary
| Setting | Value | Purpose |
|---|---|---|
chat.agent.enabled | true | Enable custom agents |
chat.agentSkillsLocations | [".github/skills"] | Auto-load skills |
chat.useAgentsMdFile | true | Use AGENTS.md |
chat.mcp.gallery.enabled | true | MCP tool access |
Integration Audit Checklist
10-category audit scoring system (5 points each, 50 total):
| # | Category | What to Check |
|---|---|---|
| 1 | Activation Events | package.json activationEvents match actual needs |
| 2 | Extension Context | context.subscriptions, secrets, globalState usage |
| 3 | Disposable Management | All disposables pushed to subscriptions |
| 4 | Command Registration | Commands in package.json match registerCommand |
| 5 | Configuration Access | getConfiguration usage, onDidChangeConfiguration |
| 6 | Webview Security | CSP policies, nonce usage, enableScripts |
| 7 | Language Model/Chat | vscode.lm patterns, tool registration |
| 8 | Telemetry | vscode.env.isTelemetryEnabled respected |
| 9 | Error Handling | try/catch patterns, error type handling |
| 10 | File System | vscode.workspace.fs vs Node.js fs |
Quick wins (high impact, low effort):
- •Telemetry opt-out: Check
vscode.env.isTelemetryEnabled - •CSP on webviews: Add Content-Security-Policy with nonce
- •Config listeners: Add
onDidChangeConfigurationfor runtime updates - •Secret storage: Use
context.secretsinstead of settings for tokens
Scoring:
- •45-50: Excellent — Ready for publish
- •40-44: Good — Minor fixes
- •35-39: Fair — Address before publish
- •<35: Needs Work — Major refactoring
When to apply: Before marketplace publishing, after major features, quarterly reviews.
Synapses
See synapses.json for connections.