agentleFS
afaraha8403afaraha8403/mental/.agents/skills/copilot-desktop-extensions/SKILL.md

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
  1. Very specific:

    Import from @github/copilot-sdk/extension.

    line 44
  2. Very specific:

    Never write to stdout (console.log).

    line 47
  3. Very specific:

    Call extensions_manage with operation: "guide".

    line 62
  4. Very specific:

    Call extensions_reload. New tools are available immediately, even mid-turn.

    line 65
  5. Very specific:

    Verify: extensions_manage { operation: "list" }, then inspect for the log tail of a failed extension.

    line 66
  6. Distinctive:

    Treat that as a shape, not a template: start from the scaffold, which is current.

    line 102
  7. Distinctive:

    Never log or echo secrets.

    line 112
  8. Distinctive:

    Do not publish, share to a gist, or install a third-party extension without the user's explicit go-ahead this turn.

    line 114

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  1. File named exactly extension.mjs. Only .mjs is supported.
  2. One immediate subdirectory per extension. Discovery is not recursive.
  3. Import from @github/copilot-sdk/extension. Never add it to package.json; the CLI resolves it.
  4. Call joinSession({ tools, hooks, canvases, requestedEnvironmentVariables }) once.
  5. Tool names are globally unique across all loaded extensions. A collision makes the second extension fail to load.
  6. Never write to stdout (console.log). Stdout is the JSON-RPC channel. Use session.log().

Where extensions live

ScopePath
Project.github/extensions/<name>/extension.mjs (relative to the git root)
Userthe Copilot config directory's extensions/<name>/
Session<session-state>/extensions/<name>/ (this session only)
Plugina 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

  1. Call extensions_manage with operation: "guide".
  2. Scaffold: extensions_manage { operation: "scaffold", kind: "basic" | "canvas", location, name }.
  3. Edit the generated extension.mjs. Split a complex canvas into sibling files.
  4. Call extensions_reload. New tools are available immediately, even mid-turn.
  5. Verify: extensions_manage { operation: "list" }, then inspect for the log tail of a failed extension.
  6. Exercise it: for a canvas, list_canvas_capabilities, open_canvas, then invoke_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.1 on an ephemeral port only. Never 0.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.mjs in an immediate subdirectory; no console.log.
  • Tool names unique; handlers return a string or { textResultForLlm, resultType }.
  • Canvas onClose frees every server, timer and watcher.
  • extensions_reload run, then list shows it loaded and inspect has 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-sdk locally.
  • Naming the entry file extension.js or index.mjs.
  • One giant file for a stateful canvas.
  • Leaving a loopback server running after onClose.

Pointers

NeedWhere
Tools, hooks, canvas API, debuggingreferences/canvas-and-tools.md
Plugins, sharing, extension dirs in a pluginreferences/plugins-and-sharing.md

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

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.