Manifest
Every gadget must contain a manifest.toml at its root. The file uses the
TOML format. This is the main entry point the host reads
when discovering a gadget. Before any WASM code runs, the host parses this file
to learn what the gadget is, what capabilities it needs, where its WASM binary
and frontend bundles are located, and how to configure its sandbox. A gadget
without a valid manifest.toml is ignored during discovery.
The following example shows all available sections. Each section and its fields are explained in detail below.
[gadget]id = "my-gadget"name = "My Gadget"description = "Does something useful"version = "0.1.0"wasm = "my_gadget.wasm"icon = "heroicons:sparkles"prefixes = ["!mg"]
[settings]enabledFeature = truethreshold = 42
[permissions]clipboard = truefrecency = true
[permissions.opener]schemes = ["https", "http"]open-path = true
[permissions.http]origins = ["https://api.example.com"]
[permissions.filesystem]read = ["${xdg-config}/myapp/config.toml"]
[[permissions.command]]binary = "my-tool"argv = [ { kind = "literal", value = "run" }, { kind = "rest", constraint = { kind = "any-string" } },]
[storage.sql]migrations = ["migrations/001_init.sql"]
[[tasks]]id = "cleanup"schedule = "*/30 * * * *"
[frontend]launcher-bundle = "frontend/dist/launcher.js"launcher-css = "frontend/dist/launcher.css"settings-bundle = "frontend/dist/settings.js"settings-css = "frontend/dist/settings.css"
[frontend.views]main = "MainView"
[frontend.inline-views]preview = "PreviewInline"
[frontend.settings]component = "MySettings"
[shortcuts]quick-open = { label = "Quick Open", default = "CmdOrCtrl+Shift+G" }Gadget identity
Section titled “Gadget identity”The [gadget] section is the only required section. It tells the host who
your gadget is, where
to find its WASM binary, and how to display it in the settings UI. The id is
particularly important: it determines the gadget’s settings namespace
(gadgets.<id>.*), its storage directory, and how other parts of the system
reference it. Choose it carefully, because changing it later means losing
existing user settings and data.
All fields except prefixes are required.
| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier. Lowercase alphanumeric, hyphens, and dots only. No leading or trailing hyphen. Should follow reverse-domain convention (see below). |
name |
string | Human-readable display name shown in settings. |
description |
string | Short description shown in the settings header. |
version |
string | Version string (e.g. "0.1.0"). |
wasm |
string | Path to the compiled WASM binary, relative to the gadget root. |
icon |
string | Either "heroicons:<name>" for a Heroicons icon, or a relative path to a bundled asset (e.g. "assets/icon.svg"). |
prefixes |
list of strings | Search prefixes for exclusive query routing. Default: []. |
The wasm field must match the compiled binary filename. Cargo converts
hyphens to underscores in crate names, so a crate named my-gadget produces
my_gadget.wasm.
Settings defaults
Section titled “Settings defaults”Most gadgets have configurable options: retention periods, feature toggles, API
tokens, display preferences. The [settings] section declares default values
for these options. When the gadget is loaded for the first time, these defaults
are written into the settings store. Once the user changes a value through the
settings panel, the user’s value takes precedence and the manifest default is
never applied again for that key.
Keys are arbitrary (camelCase by convention). Values can be any JSON-compatible type: booleans, integers, floats, strings.
[settings]heuristicEnabled = trueretentionDays = 30greeting = "Hello!"Defaults are seeded into the settings store on first load. Existing user values
are never overwritten. The gadget reads settings at runtime through the
settings host import and reacts
to changes via
on_setting_changed.
Permissions
Section titled “Permissions”Gadgets run in a sandbox. By default, no host capability is accessible. The
[permissions] section is where you declare what your gadget needs. If a
capability is not listed here, the host blocks it at runtime with a permission
error. This design lets users audit what a gadget can do before installing it.
How enforcement works
Section titled “How enforcement works”When the host instantiates a gadget, it reads the [permissions] section and
builds a compiled permission object for each declared capability. These objects
are stored on the WASM instance and checked on every host import call. The
enforcement flow is:
- The manifest declares what the gadget needs.
- The host parses and compiles the declarations at instantiation time (glob patterns are compiled, regexes are anchored, origins are normalized).
- Every host import call checks the compiled permission object before doing
any work. If the capability was not declared at all, the call returns a
permission-deniederror immediately. - For scoped permissions (HTTP, filesystem, command, opener), the host runs additional checks against the specific request (URL origin, file path, binary + arguments) using the compiled rules.
Boolean flags
Section titled “Boolean flags”Some capabilities are simple on/off grants. The gadget either has access or it
doesn’t, with no further scoping. All default to false.
| Key | Capability |
|---|---|
clipboard |
Write text to the system clipboard via clipboard. Read access is intentionally not exposed. |
frecency |
Read the gadget’s own frecency ranking data via frecency. The host records frecency automatically during execution; this flag only controls whether the gadget can query its own rankings. |
website-metadata |
Look up website titles and favicons via website-metadata. The cache is shared across all gadgets. |
settings |
Access the gadget’s own settings namespace via settings. |
sql-storage |
Use a per-gadget SQLite database via sql-storage. Also requires a [storage.sql] section declaring migrations. |
path-resolver |
Resolve substitution variables via path-resolver. |
For example, a gadget that copies results to the clipboard, uses frecency for its browse mode, and enriches URL results with website titles and favicons would declare:
[permissions]clipboard = truefrecency = truewebsite-metadata = trueOpener
Section titled “Opener”The [permissions.opener] section controls the
opener host import,
which lets gadgets open URLs in the browser, launch files with their default
application, or reveal paths in the file manager. Each of these three
operations has its own permission field, described below. At least one must be
enabled.
| Field | Type | Default | Description |
|---|---|---|---|
schemes |
list of strings | [] |
URL schemes allowed for open_url (e.g. "https", "http", "mailto"). |
open-path |
bool | false |
Allow opening filesystem paths with the OS default handler. |
reveal-path |
bool | false |
Allow revealing filesystem paths in the file manager. |
A gadget that opens web links and reveals files in the file manager, but does not launch arbitrary paths, would declare:
[permissions.opener]schemes = ["https", "http"]reveal-path = trueAt call time, the host enforces each field independently:
open_url: the host parses the URL and compares its scheme (case-insensitive) against theschemeslist. A scheme not in the list is blocked with a permission error.open_path/reveal_path: pure boolean gates. Iffalse, the call is rejected immediately regardless of which path is requested.
The [permissions.http] section controls the
http host import, which lets gadgets
make synchronous HTTP requests. Gadgets that fetch data from web APIs, download
databases, or call remote services declare their allowed origins here.
| Field | Type | Description |
|---|---|---|
origins |
list of strings | Allowed origins. Normalized at parse time. Non-empty. Use "*" for trust-all. |
A gadget that fetches data from two endpoints would declare:
[permissions.http]origins = ["https://api.example.com", "https://cdn.example.com"]At call time, the host parses the request URL and extracts its origin (scheme +
host + non-default port). A request to https://api.example.com/v1/data
produces the origin https://api.example.com, which must be in the list. Port
normalization is automatic: https://example.com:443 and
https://example.com are the same origin.
Filesystem
Section titled “Filesystem”The [permissions.filesystem] section controls the
filesystem host import, which
lets gadgets read files from the host filesystem. Gadgets that need to read
configuration files, data directories, or log files declare which paths they
may access here.
| Field | Type | Description |
|---|---|---|
read |
list of strings | Path patterns. Supports * (single segment) and ** (cross-segment) globs. Non-empty. |
Patterns can use substitution variables like ${home} or ${xdg-config} to
reference platform-specific directories without hardcoding paths. See
Substitution variables for the complete list of
available variables and where they are recognized.
A gadget that reads its own config file and a directory of JSON data files would declare:
[permissions.filesystem]read = [ "${xdg-config}/myapp/config.toml", "/var/lib/myapp/data/*.json", "${home}/.config/myapp/**",]At call time, the host performs three steps before allowing a read:
- Validation: rejects paths that are empty, relative, or contain
..,.,//, or NUL bytes. - Canonicalization: resolves all symlinks in the request path via
std::fs::canonicalize, producing the real filesystem path. - Matching: checks the real path against the compiled glob set built from the manifest patterns. A symlink inside an allowed directory that points outside the allow set is rejected because the resolved real path no longer matches.
Command execution
Section titled “Command execution”The [[permissions.command]] section controls the
command host import, which lets
gadgets run system processes. This is the most fine-grained permission: you
specify not just which binary, but what argument patterns are allowed per
position. A gadget granted git log cannot run git push.
The double-bracket [[...]] syntax is TOML’s array of tables:
each [[permissions.command]] block declares one rule, and you can repeat the
block as many times as needed to allow different binaries or different
invocation shapes of the same binary.
| Field | Type | Required | Description |
|---|---|---|---|
binary |
string | yes | Executable name or absolute path. |
argv |
list of constraints | no | Per-position argument rules. Default: [] (zero arguments only). |
cwd |
string | no | Working directory. Supports substitution variables. |
timeout-ms-max |
integer | no | Hard ceiling on per-call timeout in milliseconds. |
max-output-bytes |
integer | no | Hard ceiling on captured stdout + stderr bytes. |
max-stdin-bytes |
integer | no | Hard ceiling on stdin input bytes. |
Each element in argv constrains one argument position using an inline table
with a kind field:
| Kind | Fields | Description |
|---|---|---|
literal |
value |
Exact string match. Supports substitution variables. |
enum |
values |
Argument must equal one of the listed strings. |
glob |
pattern |
Argument must match the glob pattern. |
regex |
pattern |
Argument must match the regex (anchored to the full string automatically). |
path-under |
root |
Argument must canonicalize to a path under root. Symlinks are resolved before checking. Supports substitution variables. |
any-string |
(none) | Accepts any value unconditionally. |
rest |
constraint |
Applies the inner constraint to all remaining positions. Must be the last element in the list. |
A gadget that runs git log with arbitrary trailing arguments and mdfind
with a fixed query would declare:
[[permissions.command]]binary = "git"argv = [ { kind = "literal", value = "log" }, { kind = "rest", constraint = { kind = "any-string" } },]
[[permissions.command]]binary = "mdfind"argv = [ { kind = "literal", value = "kMDItemContentType == 'com.apple.application-bundle'" },]At call time, the host matches the binary name by exact string equality, then
walks the argument list position by position against the compiled constraints.
Without a trailing rest constraint, the argument count must match exactly.
With rest, the prefix must match and every trailing argument must satisfy the
inner constraint. If no rule matches, the call is rejected before the process is spawned.
Overlap detection
Section titled “Overlap detection”If you declare multiple rules for the same binary, their argument patterns must
be distinguishable. The host rejects the manifest at parse time if two rules
could both accept the same call, because it would be unclear which rule should
apply. For example, this is rejected because a call like git log would match both
rules:
[[permissions.command]]binary = "git"argv = [{ kind = "any-string" }]
[[permissions.command]]binary = "git"argv = [{ kind = "literal", value = "log" }, { kind = "rest", constraint = { kind = "any-string" } }]The fix is to make both rules distinguishable by using a literal first
argument in each:
[[permissions.command]]binary = "git"argv = [{ kind = "literal", value = "status" }]
[[permissions.command]]binary = "git"argv = [{ kind = "literal", value = "log" }, { kind = "rest", constraint = { kind = "any-string" } }]Substitution variables
Section titled “Substitution variables”Several permission fields support ${...} substitution variables that resolve to host-specific paths at parse time. This lets manifests reference directories like the user’s home or the gadget’s own data directory without hardcoding platform-specific paths.
| Variable | Resolves to |
|---|---|
| ${gadget-data} | The gadget’s host-managed state directory. |
| ${gadget-archive} | The gadget’s code root (archive or dev directory). |
| ${home} | The user’s home directory. |
| ${xdg-config} | XDG config directory (~/.config on Linux, ~/Library/Application Support on macOS). |
| ${xdg-data} | XDG data directory (~/.local/share on Linux, ~/Library/Application Support on macOS). |
Recognized in: [permissions.filesystem] read patterns,
[[permissions.command]] cwd, literal values, enum values, and
path-under roots.
These variables are also available at runtime through the
path-resolver host import,
so your gadget code can construct the same paths the permission rules reference.
SQL storage
Section titled “SQL storage”Gadgets that need persistent structured storage (clipboard history, calculation
logs, cached API responses) can declare a SQLite database in the [storage.sql]
section. Each gadget gets its own isolated database file managed entirely by the
host. The gadget never touches the file directly; instead it uses the
sql-storage host import to
execute queries and statements.
| Field | Type | Description |
|---|---|---|
migrations |
list of strings | Ordered list of SQL file paths relative to the gadget root. |
A gadget that creates a table and later adds an index would declare:
[storage.sql]migrations = [ "migrations/001_init.sql", "migrations/002_add_index.sql",]How migrations work
Section titled “How migrations work”Migration files are standard SQL scripts. The host applies them in the order
they appear in the migrations list before the gadget’s enable() runs, so
the database is always at the latest schema when your code first accesses it.
Naming files with a numeric prefix (001_, 002_) is recommended to keep the
directory listing consistent with the manifest order, but the filenames
themselves have no effect on execution order.
A typical initial migration creates the gadget’s tables:
CREATE TABLE history ( id TEXT PRIMARY KEY, expression TEXT NOT NULL, result TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')));A later migration can add an index or a new column:
CREATE INDEX idx_history_created_at ON history(created_at DESC);The database file is stored in the gadget’s host-managed state directory, separate from the gadget’s code. See Packaging for the storage layout.
Scheduled tasks
Section titled “Scheduled tasks”Some gadgets need to run work periodically without user interaction: cleaning up
old database rows, refreshing a cached dataset, syncing with an external
service. Since WASM gadgets cannot spawn their own threads or timers, the host
provides a cron-based scheduler. Like [[permissions.command]], the
double-bracket [[tasks]] syntax is
TOML’s array of tables, so you
can declare multiple tasks by repeating the block. Each entry declares a task
ID and a schedule. The host invokes the gadget’s
run_task export at the configured
times. Task errors are logged but do not disable the gadget. Duplicate task IDs
within a gadget are rejected at parse time.
| Field | Type | Description |
|---|---|---|
id |
string | Unique task identifier passed to run_task. |
schedule |
string | 5-field POSIX cron expression (minute hour day month weekday). |
A gadget with a cleanup task every 30 minutes and a sync task every 6 hours would declare:
[[tasks]]id = "retention-cleanup"schedule = "*/30 * * * *"
[[tasks]]id = "sync"schedule = "0 */6 * * *"Frontend bundles
Section titled “Frontend bundles”Gadgets that render custom UI in the launcher or provide their own settings panel ship React frontend bundles alongside the WASM binary. This section tells the host where to find those bundles and which JavaScript exports map to which views.
All paths are relative to the gadget root. See the Frontend section for building these bundles.
| Field | Type | Description |
|---|---|---|
launcher-bundle |
string | ES module for the launcher webview. Required if views, inline-views, or launcher-css are declared. |
launcher-css |
string | CSS file loaded alongside the launcher bundle. |
settings-bundle |
string | ES module for the settings webview. Required if [frontend.settings] or settings-css are declared. |
settings-css |
string | CSS file loaded alongside the settings bundle. |
Custom views
Section titled “Custom views”When your gadget returns a
CustomUi search response,
the view field in your ViewResponse tells the host which component to
mount. The [frontend.views] table connects that name to an actual JavaScript
export from launcher-bundle. The key is the view name you use in Rust, the
value is the named export in your JavaScript bundle.
A gadget with a grid view and a detail view would declare:
[frontend.views]grid = "EmojiGrid"detail = "EmojiDetail"When your search() returns a CustomUi response with view set to
"grid", the host looks up that name in this table and mounts the
EmojiGrid component.
Inline views
Section titled “Inline views”The [frontend.inline-views] table works the same way, but for
InlineUi search responses.
Inline views render a small component above the standard result list rather
than replacing it entirely.
A gadget that shows a quick preview above the results would declare:
[frontend.inline-views]preview = "ResultPreview"When your search() returns an InlineUi response with view set to
"preview", the host looks up that name here and mounts the ResultPreview
component above the result list.
Settings panel
Section titled “Settings panel”The [frontend.settings] section registers the settings panel component. If
your gadget has configurable options declared in [settings], you
probably want a settings panel so users can change them. This section points
the host to the React component exported from settings-bundle that renders
your gadget’s settings UI. See
Settings Panels for building these
components.
| Field | Type | Description |
|---|---|---|
component |
string | Named JavaScript export for the settings panel. |
The component name must match a named export in the settings-bundle:
[frontend.settings]component = "MySettings"Shortcuts
Section titled “Shortcuts”Some gadgets benefit from global keyboard shortcuts that work even when the launcher is closed. The Clipboard Manager uses this to let users open the clipboard history with Cmd+Shift+V from anywhere. This section declares those shortcuts. The host registers them as system-wide hotkeys when the gadget is enabled, and users can change the key combination in settings.
| Field | Type | Description |
|---|---|---|
label |
string | Description shown in settings. |
default |
string | Initial key combination (e.g. "CmdOrCtrl+Shift+V"). Users can change it in settings. |
Each key in the table is a stable shortcut ID, and the value declares the label and default binding:
[shortcuts]open-clipboard = { label = "Open Clipboard History", default = "CmdOrCtrl+Shift+V" }