Manifest — aero.json
Every extension is described by a single aero.json at the root of its .aero-ext archive. This page is the complete field reference, generated from aero-manifest.schema.json. The main process validates every manifest against that schema on install; the aero-ext CLI validates it on package.
A complete example
{
"name": "dracula-warm",
"displayName": "Dracula Warm",
"publisher": "aero",
"version": "1.0.0",
"description": "A cozy, warm take on Dracula for Aero — plus a command to apply it.",
"tagline": "Cozy warm dark colors",
"category": "Themes",
"icon": "🧛",
"tags": ["dark", "warm", "theme"],
"engines": { "aero": ">=0.5.0" },
"main": "extension.js",
"activationEvents": ["onCommand:dracula-warm.apply"],
"contributes": {
"themes": [
{ "label": "Dracula Warm", "uiTheme": "dark", "path": "themes/dracula-warm.json" }
],
"commands": [
{ "command": "dracula-warm.apply", "title": "Dracula Warm: Apply Theme" }
],
"keybindings": [
{ "key": "cmd+k cmd+d", "command": "dracula-warm.apply", "when": "always" }
],
"snippets": [
{ "language": "javascript", "path": "snippets/javascript.json" }
]
}
}Top-level fields
The schema sets additionalProperties: false — unknown top-level keys are a validation error.
| Field | Type | Required | Constraint |
|---|---|---|---|
name | string | yes | ^[a-z0-9][a-z0-9-]*$, ≤ 64 chars. kebab-case. |
displayName | string | yes | 1–80 chars. Human title shown on cards. |
publisher | string | yes | ^[a-z0-9][a-z0-9-]*$, ≤ 64 chars. |
version | string | yes | ^\d+\.\d+\.\d+$ (semver x.y.z). |
description | string | no | ≤ 2000 chars. Long marketplace copy. |
tagline | string | no | ≤ 80 chars. One-liner for cards. |
category | string | no | One of the categories. Default Tools. |
icon | string | no | ≤ 8 chars. Emoji/glyph preferred — no asset deps. |
tags | string[] | no | ≤ 12 items, each ≤ 32 chars. |
engines | object | no | { "aero": "<semver range>" }. |
main | string | no | Relative JS entry for command logic. See below. |
activationEvents | string[] | no | See Activation events. |
contributes | object | no | The contributions. See below. |
name
The machine name, kebab-case, unique within a publisher. Combined with publisher it forms the canonical id "<publisher>.<name>" (e.g. aero.dracula-warm). That id is the registry primary key and the on-disk folder name — it is derived, not stored in the manifest.
version
Strict three-part semver (1.0.0). Published versions are immutable — to ship a fix, bump the version and publish again. The registry enforces a unique (extension_id, version).
category
If present, must be one of:
Themes · Languages · Linters · Formatters · Tools · AI · SnippetsThese match the marketplace's CATEGORIES. Omit it to default to Tools.
icon
An emoji or single glyph (≤ 8 chars). Aero prefers emoji over image assets so extensions stay dependency-free and tiny. (An icon.png may sit in the archive, but the manifest emoji is the recommended path.)
engines.aero
Optional semver range checked against the running app version, e.g. { "aero": ">=0.5.0" }. Use it to gate on APIs introduced in a given Aero release.
main
Relative path to a sandboxed JS entry, used only for command logic. The pattern forbids a leading slash and any backslash (^[^/\\][^\\]*$) — paths are relative and forward-slash. Theme-, snippet-, and keybinding-only extensions need no main. See The aero.* API.
Code runs sandboxed
main JS executes inside a sandboxed <iframe sandbox="allow-scripts"> with no DOM, Node, or window.api access. Its only channel is a postMessage bridge to the capability-gated aero.* API. Declarative contributions carry no code at all.
contributes
The container for the four v1 contribution types. additionalProperties: false applies here too, so only these keys are valid.
"contributes": {
"themes": [ /* … */ ],
"commands": [ /* … */ ],
"keybindings": [ /* … */ ],
"snippets": [ /* … */ ]
}contributes.themes[]
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | yes | Name shown in the theme list (≥ 1 char). |
uiTheme | string | yes | "light" or "dark" — base palette. |
path | string | yes | Relative path to the theme JSON file. |
Full token list and theme-file shape: Themes.
contributes.commands[]
| Field | Type | Required | Notes |
|---|---|---|---|
command | string | yes | Id, ^[A-Za-z0-9_.-]+$. Convention name.verb. |
title | string | yes | Palette label (≥ 1 char). |
Details and JS handlers: Commands.
contributes.keybindings[]
| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | Space-separated chords (≥ 1 char). |
command | string | yes | ^[A-Za-z0-9_.-]+$. Own or built-in command. |
when | string | no | "editorFocus" or "always". |
Chord syntax and contexts: Keybindings.
contributes.snippets[]
| Field | Type | Required | Notes |
|---|---|---|---|
language | string | yes | Monaco language id (≥ 1 char). |
path | string | yes | Relative path to the snippet JSON file. |
Snippet-file shape: Snippets.
Path rules (all path fields)
Every path in the manifest — main, themes[].path, snippets[].path — must:
- be relative and forward-slash separated;
- resolve inside the archive root;
- contain no
.., no absolute paths, and no symlinks.
The packager and the main process both reject paths that escape the archive.
Validating locally
npx aero-ext package # validates aero.json against the schema, then zipsYou can also validate aero.json against the published schema directly. Add the $schema key in your editor for inline hints:
{
"$schema": "https://aeroide.in/schemas/aero-manifest.schema.json",
"name": "my-extension"
}