Skip to content

Canvas extensions (Experimental)

A canvas is a GitHub Copilot CLI extension: a directory bundle whose entry file is extension.mjs (executable Node.js), plus any sibling assets it needs. Copilot CLI discovers canvases in immediate subdirectories of .github/extensions/<name>/ (project scope) and ~/.copilot/extensions/<name>/ (user scope). APM lets a package carry a canvas under .apm/extensions/<name>/ and deploys it to the matching location at install time so the canvas is available in your Copilot session.

Canvases are typically produced by the Copilot CLI create-canvas skill (scaffolds a working extension in .github/extensions/). This page covers how to ship one through an APM package.

Terminal window
apm experimental enable canvas

Default APM behaviour never changes until the flag is enabled. With the flag off, .apm/extensions/ is ignored entirely.

Place the bundle under your package’s .apm/ directory. The marker file is extension.mjs; a directory without it is ignored.

.apm/
extensions/
my-canvas/
extension.mjs # required entry point (executable Node.js)
ui.js # optional sibling assets
styles.css

The <name> segment becomes both the deploy directory and the extension id, so it is validated strictly: [A-Za-z0-9._-]+, no leading or trailing dot, no .., no path separators, and reserved device names are rejected.

Terminal window
apm install --target copilot

APM deploys the bundle verbatim to .github/extensions/my-canvas/. The deploy is atomic: every file in the bundle is planned and validated first, and any unmanaged local collision skips the whole bundle (use apm install --force to overwrite) so you never end up with a half-updated executable extension.

After a canvas deploys, start a new Copilot CLI session (exit and relaunch) — Copilot CLI discovers extensions at session start, so a freshly-deployed canvas is not picked up mid-session.

A canvas shipped by a dependency is arbitrary executable Node.js code. When the project opts in to the executable gate (by adding an executables: block to apm.yml), APM blocks dependency-provided canvases unless the package has been explicitly approved. To deploy them:

# apm.yml (committed -- opts the project in to the gate)
executables: {}
Terminal window
# apm approve writes committed project trust (shared with the team);
# add --user to record a personal grant in ~/.apm/config.json instead.
apm approve some-org/canvas-package
apm install --target copilot

By default apm approve writes the grant to the project apm.yml executables.allow block (committed), so the trust decision is shared with the team. apm approve --user records a personal grant in ~/.apm/config.json instead — a machine-local override that is never committed. Adding an empty executables: {} enables the gate but grants trust to nothing; approve each package you want to deploy.

The legacy top-level allowExecutables: block is a deprecated alias for executables.allow, read for one minor cycle and migrated on the next apm approve / apm deny write.

The trust gate is independent of the experimental flag:

  • The experimental flag decides whether the canvas primitive is processed at all. It is a feature-availability gate, not a security gate.
  • The executables block decides whether dependency canvases may deploy. Your own first-party canvas (in the root package you are installing from) deploys freely once the flag is on; only dependency-provided canvases need approval.

When a dependency canvas is blocked, APM prints a diagnostic naming the package and the canvas, and instructs you to run apm approve <pkg>. The same gate is enforced on offline bundle install (apm install <bundle>), so a vendored bundle cannot smuggle an executable canvas past trust.

To make a canvas available in every Copilot session, install it globally so it lands in ~/.copilot/extensions/<name>/:

Terminal window
apm approve <package>
apm install <package> --global

Global canvas install is intentionally limited in this experimental release:

  • Dependency-provided only. Only a canvas shipped by a package you install (the --global flow always treats the canvas as dependency-provided) deploys globally, so APM records it in the user lockfile and apm uninstall --global can prune it. A first-party root .apm/extensions/ canvas is not deployed at user scope — package it and install it as a dependency instead.
  • Approval is always required. A global canvas has full-account blast radius, so executable-trust approval is mandatory even though the project-scope first-party path does not need it.
  • Default ~/.copilot only. If $COPILOT_HOME is set to a non-default location, APM refuses the global canvas install rather than deploy to a path Copilot will not scan.

apm uninstall --global <package> removes the deployed ~/.copilot/extensions/<name>/ files and prunes the empty directories.

apm pack preserves .apm/extensions/ in the bundle, so a packed package keeps its canvas. apm uninstall removes the deployed .github/extensions/<name>/ files and prunes the now-empty directories; uninstall is never gated by the experimental flag, so a previously-installed canvas can always be removed.

  • Copilot-only. A canvas is a Copilot CLI construct. Other targets (--target claude, cursor, etc.) never receive it.
  • Global install is dependency-only. User-scope (--global) deployment to ~/.copilot/extensions/ supports dependency-provided canvases (always requiring executable-trust approval) and the default ~/.copilot location only; first-party root canvases deploy at project scope only.
  • No compile/list surfacing yet. Canvases are not yet shown by apm list/apm compile; they are deployed at install only.
  • No canvas-specific org policy field yet. The org executables: block in apm-policy.yml governs canvas trust alongside the other executable types (deny_all, deny, require, recommend); a canvas-only policy knob is not planned for this experimental release.

See the primitives and targets matrix for where the canvas primitive sits.