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 | Semantic version (e.g. "0.1.0"). Shown after the gadget’s name in settings. |
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.
When a user installs a new version of your gadget over an installed one, the
host compares the two version values to tell an update from an older
version, and warns the user before an older one replaces a newer one. A value
that isn’t a valid semantic version still installs, but the review can’t say
which of the two is newer. See
Publishing an update.
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.
Users see these declarations before they install your gadget, and later on your gadget’s entry in Settings → Gadgets. Both list every permission, grouped by what it touches: programs, network, files, links, the clipboard, and the gadget’s own data. Installing Gadgets shows the groups from the user’s side.
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. Requires a [storage.sql] section declaring migrations; a manifest with only one of the two fails to load. |
path-resolver |
Resolve substitution variables via path-resolver. |
icon-cache |
Show the real icon of an installed application with the AppIcon entry icon. Without this flag, an entry with AppIcon is shown without an icon. |
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"), as bare names without : or //, or "*" for any scheme. |
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: scheme, host and optional port, such as "https://api.example.com", without a path, query or fragment. 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 | Absolute path patterns: each starts with /, a substitution variable or, for Windows, a drive letter (C:\). 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 | Absolute path, or a bare program name looked up on a fixed PATH (see What the program sees). A relative path such as bin/tool is rejected. |
argv |
list of constraints | no | Per-position argument rules. Default: [] (zero arguments only). |
cwd |
string | no | The folder the program runs in, and the root of the folders your gadget may pass with .cwd(). Supports substitution variables. See Working directory. |
env |
table | no | Environment variables your gadget may set, each with a value constraint. See Environment variables. |
timeout-ms-max |
integer | no | Hard ceiling on per-call timeout in milliseconds, at least 1. Not enforced yet, see below. |
max-output-bytes |
integer | no | Hard ceiling on captured stdout + stderr bytes, at least 1. Not enforced yet, see below. |
max-stdin-bytes |
integer | no | Hard ceiling on stdin input bytes, at least 1. Not enforced yet, see below. |
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. A pattern that does not compile fails the manifest. |
regex |
pattern |
Argument must match the regex (anchored to the full string automatically). |
path-under |
root |
Argument must be an absolute path without .. that canonicalizes to a path under root. Symlinks are resolved before checking, and a path through a symlink whose target does not exist is rejected. The path must neither lie in nor contain Torchsnap’s data folder (see Files your gadget can write). root starts with / or a variable and 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.
Working directory
Section titled “Working directory”A rule’s cwd decides where the program runs and which folders your
gadget may choose. Without cwd, the program runs in an empty folder that
exists only for that call, and a call that passes .cwd() is rejected.
With cwd, a call without .cwd() runs in that folder, which is created
if it does not exist, and a call with .cwd() must pass an absolute path
without .. to that folder or a folder inside it.
A gadget that runs git log in repositories below the user’s projects
folder would declare:
[[permissions.command]]binary = "git"argv = [ { kind = "literal", value = "log" }, { kind = "rest", constraint = { kind = "any-string" } },]cwd = "${home}/projects"The host resolves the folder your gadget passes, symlinks included, and
starts the program in the resolved folder. A folder outside cwd, a
relative path, or a path through a symlink whose target does not exist
fails with permission-denied. The folder must also neither lie in nor
contain Torchsnap’s data folder, so your home folder, ~/Library and
~/Library/Application Support cannot be a working directory, not even
as the default of cwd = "${home}". Declare a narrower cwd instead.
Environment variables
Section titled “Environment variables”Your gadget can set an environment variable only when the rule declares
it. The [permissions.command.env] table lists the variables, each with
a value constraint of the same kinds as argv positions, except rest:
[[permissions.command]]binary = "git"argv = [{ kind = "literal", value = "log" }]
[permissions.command.env]GIT_AUTHOR_NAME = { kind = "regex", pattern = "[A-Za-z ]{1,64}" }LC_ALL = { kind = "enum", values = ["C", "en_US.UTF-8"] }A call that sets an undeclared variable, a value with a NUL byte, or a
value outside its constraint fails with permission-denied. A value
checked by regex, glob or any-string must not contain /: to pass
a path, declare the variable as path-under, and the program receives
the resolved path. A literal or enum value may name only absolute
paths, also inside a list or after --option=.
The manifest is rejected when a key is empty or contains =, and for
variables that make a program load or run other code, in any case:
PATH, HOME, TMPDIR, every variable starting with LD_, DYLD_,
GIT_CONFIG_KEY_, GIT_CONFIG_VALUE_ or npm_config_, and
NODE_OPTIONS, NODE_PATH, PYTHONPATH, PYTHONSTARTUP, PYTHONHOME,
PERL5LIB, PERL5OPT, RUBYOPT, RUBYLIB, JAVA_TOOL_OPTIONS,
BASH_ENV, ENV, IFS, ZDOTDIR, PROMPT_COMMAND, SHELL, PAGER,
LESSOPEN, EDITOR, VISUAL, BROWSER, SSH_ASKPASS, GIT_SSH,
GIT_SSH_COMMAND, GIT_EXTERNAL_DIFF, GIT_PAGER, GIT_EDITOR,
GIT_SEQUENCE_EDITOR, GIT_ASKPASS, GIT_PROXY_COMMAND,
GIT_EXEC_PATH, GIT_CONFIG_COUNT and GIT_CONFIG_PARAMETERS.
What the program sees
Section titled “What the program sees”The program does not inherit Torchsnap’s environment. It gets LANG,
LANGUAGE, LC_*, TZ and __CF_USER_TEXT_ENCODING from Torchsnap,
PATH set to
/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin, the
variables your gadget declared and set, and HOME and TMPDIR pointing
at empty folders that exist only for that call. A bare binary name is
looked up on that PATH; a program installed elsewhere needs an
absolute binary. Programs that find their configuration through
HOME, such as git, do not see the user’s files. Programs that look up
the home folder in the system’s user database, such as ssh, still do.
Files your gadget can write
Section titled “Files your gadget can write”Nothing your gadget can write may act as configuration for the program. Torchsnap’s data folder holds every gadget’s data, the installed gadgets and Torchsnap’s own settings, so the host keeps it out of every command:
- The manifest is rejected when a rule’s
cwd, apath-underroot, or aliteralorenumvalue uses ${gadget-data}, or, forcwdandenvvalues, ${gadget-archive}. Arguments may name files in your gadget’s package, such as a script a granted interpreter runs. Roots and paths in fixed values may not contain... - The install review refuses a gadget, and loading fails, when a rule’s
resolved
cwd,path-underroots or fixed absolute paths lie in Torchsnap’s data folder. - A call fails with
permission-deniedwhen the folder it runs in, or a path it passes for apath-underargument or variable, lies in or contains that folder.
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, and in
[[permissions.command]] cwd, literal values, enum values and
path-under roots, both in argv and in env.
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. The section goes together with
sql-storage = true under [permissions]; a manifest with
only one of the two fails to load.
| 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:
[permissions]sql-storage = true
[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.
Migrations follow the same rules as the rest of your gadget’s SQL (see What your SQL can reach). If the host refuses a statement in a migration, your gadget fails to enable.
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" }