Go from an empty folder to a working “Hello World” command running inside a live Extension Development Host — the foundations every VS Code extension is built on
This part takes you from zero to a running extension. By the end you will have scaffolded, coded, run and debugged your first command.
yo codepackage.json — the manifestsrc/extension.ts entry pointtsconfig.json & .vscodeignoreactivate() & deactivate()context.subscriptionsDeveloper: Reload WindowAn extension is a Node.js module that VS Code loads to add new behaviour. The editor exposes the vscode API; your code contributes features through it — almost everything except the core text-editing engine is built this way.
VS Code itself ships many of its built-in features as extensions — you are using the same API that the core team uses.
VS Code is an Electron app. Your extension does not run in the UI; it runs in a separate Extension Host — a Node.js process that talks to the editor over RPC.
Extension code has no document or window. To render custom UI you use a Webview (covered in Part 4) and message it over a postMessage bridge — never reach into the editor's HTML.
The toolchain is small and all free. Install these before scaffolding your first extension.
The Extension Host runs on Node. Use the current LTS (18+). npm ships with it.
The editor itself — it provides the Extension Development Host and the debugger.
The generator can initialise a repository for you and is needed for publishing later.
Recommended (and what this series uses). Installed locally per-project — no global install needed.
# Check Node.js and npm are installed
node --version
# v20.11.1
npm --version
# 10.2.4
# Check git
git --version
# git version 2.43.0
# Check VS Code from the command line
code --version
# 1.90.0
code CommandIf code is not found, open VS Code and run Shell Command: Install 'code' command in PATH from the Command Palette.
Match your engines.vscode manifest field to a version you actually have installed, so the dev host can launch.
The official generator, generator-code, runs through Yeoman (yo) and creates a ready-to-run project — manifest, TypeScript config, entry point and a launch task.
# Install the generator (one-off, global)
npm install -g yo generator-code
# Run it in an empty folder
yo code
? What type of extension do you want to create?
❯ New Extension (TypeScript)
New Extension (JavaScript)
New Color Theme
New Language Support
New Code Snippets
New Keymap
New Extension Pack
New Web Extension (TypeScript)
? What's the name of your extension? Hello World
? What's the identifier? hello-world
? What's the description?
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm
package.json manifest pre-filledsrc/extension.ts with activate / deactivate.vscode/launch.json wired for F5npm install already run for youYou can scaffold without installing globally:
npx --package yo --package generator-code \
-- yo code
cd hello-world
code .
hello-world/
├── .vscode/
│ ├── launch.json ▸ F5 debug config
│ └── tasks.json ▸ build task (tsc)
├── src/
│ └── extension.ts ▸ entry point
├── node_modules/ ▸ deps (gitignored)
├── .vscodeignore ▸ files to exclude
│ from the .vsix
├── package.json ▸ the manifest
├── tsconfig.json ▸ TS compiler config
├── .gitignore
└── README.md ▸ shown on Marketplace
package.jsonThe extension manifest. Declares metadata, the entry point, activation events and everything your extension contributes.
src/extension.tsYour code. Exports activate() and optionally deactivate(). Compiled to out/extension.js by default.
tsconfig.json & .vscode/tsconfig.json targets a Node-compatible build; launch.json and tasks.json let F5 compile and launch the dev host.
.vscodeignore & node_modules.vscodeignore keeps source, tests and dev deps out of the published .vsix; node_modules is installed locally and gitignored.
package.json — the Manifest{
"name": "hello-world",
"displayName": "Hello World",
"description": "My first VS Code extension",
"publisher": "my-publisher",
"version": "0.0.1",
"engines": {
"vscode": "^1.90.0"
},
"categories": ["Other"],
"main": "./out/extension.js",
"activationEvents": [],
"contributes": {
"commands": [
{
"command": "hello-world.helloWorld",
"title": "Hello World"
}
]
},
"scripts": {
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.90.0",
"typescript": "^5.4.0"
}
}
name + publisher form the unique ID publisher.namedisplayName / description show on the Marketplaceversion — semantic versioningengines.vscode — minimum API version supportedmain — the compiled entry file to loadactivationEvents — when to wake the extensioncontributes — commands, menus, settings, themes…Since VS Code 1.74, an onCommand event for each contributed command is inferred automatically — so activationEvents can stay empty here.
src/extension.tsEvery extension exports an activate() function. VS Code calls it once, the first time an activation event fires — not at startup unless you ask for that.
import * as vscode from 'vscode';
// Called once, on first activation event
export function activate(context: vscode.ExtensionContext) {
console.log('hello-world is now active!');
// ... register commands, providers, listeners here ...
// Each registration returns a Disposable.
}
// Called when the extension is deactivated
// (optional — omit if there is nothing to tidy up)
export function deactivate() {
// close connections, flush state, etc.
}
activate(context)ExtensionContextsubscriptions — array of disposables to clean upextensionUri / extensionPath — on-disk locationglobalState / workspaceState — persisted storagesecrets — secure secret storagedeactivate()Optional. Use it only for teardown that disposables don't already handle.
A command has two halves: declared in the manifest (so it appears in the palette) and implemented in code with registerCommand. The command ID must match exactly.
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// The ID must match contributes.commands in package.json
const disposable = vscode.commands.registerCommand(
'hello-world.helloWorld',
() => {
vscode.window.showInformationMessage('Hello World!');
}
);
// Push the disposable so VS Code disposes it on deactivate
context.subscriptions.push(disposable);
}
export function deactivate() {}
registerCommandBinds a command ID to a callback and returns a Disposable. Invoked from the palette, a keybinding, a menu, or programmatically via executeCommand.
context.subscriptionsPush every disposable here. When the extension deactivates, VS Code disposes them all — no manual cleanup, no leaks.
Registering the same command ID twice — or a mismatch between the manifest ID and the code — throws on activation. Keep them identical.
Hit F5. VS Code compiles your TypeScript, then launches a second window — the Extension Development Host — with your extension loaded.
Edit code in your original window; it runs in the dev host. The title bar of the dev host is marked [Extension Development Host].
Extensions are activated lazily for fast startup. An activation event tells VS Code when to load and run activate(). Declare them in activationEvents.
| Event | Fires when… |
|---|---|
onCommand:… | A contributed command is invoked (auto-inferred since 1.74) |
onLanguage:… | A file of that language ID is opened |
onStartupFinished | Shortly after the window finishes loading |
workspaceContains:… | The workspace has a file matching a glob |
onView:… | A contributed tree view is expanded |
onDebug | A debug session is about to start |
* | On startup — avoid; hurts performance |
Since VS Code 1.74, events for contributes entries (commands, views, languages…) are inferred automatically. For a command-only extension, leave activationEvents empty.
onStartupFinished* unless truly unavoidableWhichever event fires first triggers a single activate() call. Subsequent triggers just invoke the relevant handler.
The dev host launches with the debugger already attached. You debug an extension exactly like any Node program — breakpoints, stepping, watches and all.
extension.tsconsole.log(…) output appears in your original window's Debug Consolevscode namespace interactivelywatch task recompiles TS on saveIf a breakpoint is “unbound”, check the watch task compiled successfully and that source maps are enabled in tsconfig.json.
yo code scaffolds a ready-to-run TypeScript projectpackage.json is the manifest — identity, engine, contributesactivate() runs once, lazily, on an activation eventcontext.subscriptionsyo codecontributes.commands and register itcontext@vscode/vsce (package & publish), @vscode/test-cli (testing), the Contribution Points reference — all covered later in the series.
Commands & Contribution Points — menus, keybindings, the palette, command arguments, when clauses and wiring real UI into the editor.