copilot-desktop-extensions
Skill5 starsChanged yesterdayMITView on GitHub
Build extensions for the GitHub Copilot desktop app and Copilot CLI: the extension.mjs Node process, joinSession, custom tools, lifecycle hooks, and canvases (side-panel UI the agent can open and drive) with no MCP server. Covers discovery paths, scaffolding, reload and debugging with extensions_manage, sensitive env vars, sharing via gists, and shipping extensions inside a Copilot plugin. Use when the user wants to create, debug or share a Copilot desktop extension, canvas or tool.
- Reads credentials
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 3 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 44
Import from @github/copilot-sdk/extension.
- Very specific: line 47
Never write to stdout (console.log).
- Very specific: line 62
Call extensions_manage with operation: "guide".
- Very specific: line 65
Call extensions_reload. New tools are available immediately, even mid-turn.
- Very specific: line 66
Verify: extensions_manage { operation: "list" }, then inspect for the log tail of a failed extension.
- Distinctive: line 102
Treat that as a shape, not a template: start from the scaffold, which is current.
- Distinctive: line 112
Never log or echo secrets.
- Distinctive: line 114
Do not publish, share to a gist, or install a third-party extension without the user's explicit go-ahead this turn.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Copilot desktop extensions (no MCP needed)
Copilot is the one platform here where "mods" are first-class and need no MCP server. An extension is a separate Node.js process the CLI forks and talks to over JSON-RPC on stdio. It can add tools, register hooks, request env vars, and register canvases. Canvases are the UI: a panel in the app that the agent opens (open_canvas) and drives (invoke_canvas_action).
Verified against the SDK typings (canvas.d.ts, generated/rpc.d.ts): createCanvas and open returning { url, title, status } for a web-rendered panel are real. They are marked @experimental ("may change or be removed in future SDK or CLI releases"), and CanvasHostContextCapabilities.canvases says whether the host renders them. Check that capability and re-read the typings before relying on a canvas shape. The SDK docs (extensions.md, agent-author.md) do not mention canvases; the typings and extensions_manage scaffold are the source.
Source of truth, in order: the extensions_manage tool with operation: "guide" (always call it before writing code), then the SDK docs shipped with the desktop app (extensions.md, agent-author.md, examples.md, index.d.ts under the app's copilot-sdk\docs folder; the path is machine-specific, so locate it rather than assuming).
Requirements
- File named exactly
extension.mjs. Only.mjsis supported. - One immediate subdirectory per extension. Discovery is not recursive.
- Import from
@github/copilot-sdk/extension. Never add it topackage.json; the CLI resolves it. - Call
joinSession({ tools, hooks, canvases, requestedEnvironmentVariables })once. - Tool names are globally unique across all loaded extensions. A collision makes the second extension fail to load.
- Never write to stdout (
console.log). Stdout is the JSON-RPC channel. Usesession.log().
Where extensions live
| Scope | Path |
| Project | .github/extensions/<name>/extension.mjs (relative to the git root) |
| User | the Copilot config directory's extensions/<name>/ |
| Session | <session-state>/extensions/<name>/ (this session only) |
| Plugin | a plugin that declares extension directories |
A project extension shadows a user extension of the same name. Pick a scope on purpose: project for repo tooling that teammates share, user for personal tools, session for experiments.
Workflow
- Call
extensions_managewithoperation: "guide". - Scaffold:
extensions_manage { operation: "scaffold", kind: "basic" | "canvas", location, name }. - Edit the generated
extension.mjs. Split a complex canvas into sibling files. - Call
extensions_reload. New tools are available immediately, even mid-turn. - Verify:
extensions_manage { operation: "list" }, theninspectfor the log tail of a failed extension. - Exercise it: for a canvas,
list_canvas_capabilities,open_canvas, theninvoke_canvas_action.
Canvas in one screen
A canvas has an id, displayName, description, optional inputSchema, actions, an open(ctx) that returns { title, url }, and onClose(ctx). The scaffold serves HTML from a loopback server per ctx.instanceId.
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import http from "node:http";
const servers = new Map();
const status = createCanvas({
id: "status",
displayName: "Status",
description: "Shows project status the agent can update.",
actions: [{ name: "set", description: "Replace the text", handler: (ctx) => { state.text = String(ctx.input?.text ?? ""); } }],
async open(ctx) {
let entry = servers.get(ctx.instanceId);
if (!entry) {
const server = http.createServer((_q, res) => res.end(`<p>${escapeHtml(state.text)}</p>`));
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
entry = { server };
servers.set(ctx.instanceId, entry);
}
return { title: "Status", url: `http://127.0.0.1:${entry.server.address().port}/` };
},
onClose(ctx) { servers.get(ctx.instanceId)?.server.close(); servers.delete(ctx.instanceId); },
});
const state = { text: "ready" };
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
await joinSession({ canvases: [status] });Treat that as a shape, not a template: start from the scaffold, which is current. Full API detail: references/canvas-and-tools.md.
Sensitive environment variables
The CLI strips secrets such as GITHUB_TOKEN from extension processes. An extension asks by name through requestedEnvironmentVariables; the user is prompted and the approval is remembered per exact name set. A denial makes joinSession reject and the extension does not load. Request only what is needed.
Safety
- Bind canvas servers to
127.0.0.1on an ephemeral port only. Never0.0.0.0. - Escape every value interpolated into canvas HTML. Tool and action input is untrusted.
- Never log or echo secrets. Never hard-code tokens in an extension.
- A project extension runs with the user's permissions the moment a teammate opens the repo. Review
.github/extensions/changes as code, and do not auto-install extensions from untrusted gists. - Do not publish, share to a gist, or install a third-party extension without the user's explicit go-ahead this turn.
Checklist
extension.mjsin an immediate subdirectory; noconsole.log.- Tool names unique; handlers return a string or
{ textResultForLlm, resultType }. - Canvas
onClosefrees every server, timer and watcher. extensions_reloadrun, thenlistshows it loaded andinspecthas no errors.- Only the env vars it truly needs are requested.
Anti-patterns
- Adding an MCP server for something an extension tool or canvas already does.
- Installing
@github/copilot-sdklocally. - Naming the entry file
extension.jsorindex.mjs. - One giant file for a stateful canvas.
- Leaving a loopback server running after
onClose.
Pointers
| Need | Where |
| Tools, hooks, canvas API, debugging | references/canvas-and-tools.md |
| Plugins, sharing, extension dirs in a plugin | references/plugins-and-sharing.md |
In this repository30 other files it gives its agents.
- docs/agents.mdAGENTS.md
- website/AGENTS.mdAGENTS.md
- .github/copilot-instructions.mdCopilot instructions
- .cursor/rules/claude-desktop-skills.mdcCursor rule
- .cursor/rules/claude-mod.mdcCursor rule
- .cursor/rules/codex-desktop-skills.mdcCursor rule
- .cursor/rules/desktop-and-publishing-skills.mdcCursor rule
- .cursor/rules/release.mdcCursor rule
- claude-code-mods-guideSkill
- claude-desktop-extensionsSkill
- claude-desktop-modsSkill
- claude-desktop-pluginsSkill
- codex-desktop-extensionsSkill
- codex-desktop-pluginsSkill
- codex-desktop-skillsSkill
- copilot-desktop-extensionsSkill
- cursor-extensions-and-pluginsSkill
- opencode-pluginsSkill
- publishing-mental-to-storesSkill
- claude-code-mods-guideSkill
- claude-desktop-extensionsSkill
- claude-desktop-modsSkill
- claude-desktop-pluginsSkill
- codex-desktop-extensionsSkill
- codex-desktop-pluginsSkill
- codex-desktop-skillsSkill
- cursor-extensions-and-pluginsSkill
- opencode-pluginsSkill
- publishing-mental-to-storesSkill
- mental-setupSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

