Launcher manifests
A launcher manifest is a TOML file that tells mnml about one integration — its id and label, its chip visuals, the palette commands it registers, and any statusline / context-menu surfaces it wants. The same schema covers both flavors of integration:
- Pure launchers —
binaryfield omitted; commands shell out via templatedrunstrings to whatever CLI is on the user’s$PATH. - Binary siblings —
binary = "mnml-xxx"; commands typically fire:term mnml-xxxand the compiled sibling takes it from there.
Everything is optional except id and label — a two-line file is a valid manifest. See Integrations overview for the model this fits into; this page is the field reference.
Where a manifest lives
Section titled “Where a manifest lives”<workspace>/.mnml/integrations/<id>.toml # workspace-local (higher precedence)~/.config/mnml/integrations/<id>.toml # user-global~/.config/mnml/integrations/<id>.override.toml # user sidecar — merged over the baseWorkspace beats user on id collision. The id field inside the file is authoritative — the filename is convention only.
To re-scan without a restart: :integrations.refresh (palette).
The sidecar <id>.override.toml is documented in Installing → override sidecar semantics. Manifest authors don’t emit override files — those are user-owned; the manifest defines the base.
Minimal launcher
Section titled “Minimal launcher”id = "htop"label = "htop"
[[commands]]id = "htop.open"title = "htop: open"run = ":term htop"Three lines of identity, three lines of one palette command. No chip, no keybinding, no context menu. The command shows up in Ctrl+Shift+P as “htop: open” and runs htop in a Pty pane when accepted.
Full schema
Section titled “Full schema”Everything below is optional except where noted.
Identity
Section titled “Identity”id = "slack" # required · unique stable sluglabel = "Slack" # required · ~20 chars · chip hover / pane titledescription = "Slack browse + post" # one-sentence longer form for detail paneversion = "0.1.0" # semver; renders in detail-pane bylinebinary = "mnml-msg-slack" # None (omitted) = pure launchercategory = "msg" # msg / forge / tracker / aws / db / fs / test / …binarydecides the integration’s flavor. Omitted → pure launcher (no compiled sibling;runtemplates carry the whole behavior). Set → binary sibling (mnml expectsmnml-msg-slackon$PATHwhen a command runs:term mnml-msg-slack).labelreplacedchip.tooltipin 2026-08. Same value, promoted to the top level since it renders as more than a hover — the chip hover, the tree row label in the Integrations panel, the picker row, the detail-pane header.categoryis display-only — it drives the section header in the discovery overlay. Any string is legal; no closed set.
Detail-pane metadata for Pane::IntegrationDetail (all optional, all clickable links):
homepage = "https://slack.com"repository = "https://github.com/…/mnml-msg-slack"docs = "https://…/docs"author = "Ada Lovelace"[chip] — rail visuals
Section titled “[chip] — rail visuals”[chip]glyph = "\u{F0668}" # single Nerd Font codepointfallback = "Sk" # 2-char text for --ascii modecolor = "purple" # named theme colorenabled = true # rendered by default (default: true)in_palette_bar = false # false → rail INTEGRATIONS sectionbadge_key = "slack" # section id for activity-bar badgesNamed theme colors accepted by mnml: red, orange, yellow, green, blue, cyan, teal, purple, pink, comment, magenta, fg, bg2. Unknown names fall back to cyan at render.
Two SVG-glyph fields cover the “sibling ships its own icon” story (see Building integrations → sibling icons):
glyph_svg = "slack.svg" # sibling copies this via mnml-bridgeglyph_codepoint = "F1B00" # explicit codepoint for the SVG bakeThe manifest’s own glyph takes precedence if set — glyph_svg only kicks in when glyph is empty AND an SVG has been dropped in ~/.config/mnml/glyphs/. glyph_codepoint is trusted; no range check. Uppercase hex, no U+ prefix.
[[commands]] — palette entries
Section titled “[[commands]] — palette entries”[[commands]]id = "slack.open" # requiredtitle = "Slack: open" # required · palette rowgroup = "integrations" # optional · palette groupingkeys = ["<leader>iS"] # optional · multiple allowedrun = ":term mnml-msg-slack" # required · ex-command linerunis an ex-command string. Leading:is optional; either shape works. Any template variable in the string gets expanded before dispatch.keysare which-key chord specs. Multiple entries wire multiple chords to the same command; each entry is one keybinding.groupis a display hint for palette grouping — it doesn’t affect execution.
You can declare as many commands as you like — one for the primary launch, secondary ones for common actions:
[[commands]]id = "postgres.open"title = "PostgreSQL: open"run = ":term mnml-db-postgres"
[[commands]]id = "postgres.dump"title = "PostgreSQL: dump active schema"run = ":term pg_dump --schema-only {{env:DATABASE_URL}}"[[context_menu]] — right-click additions
Section titled “[[context_menu]] — right-click additions”[[context_menu]]target = "tree.file" # tree.file | tree.dir | tab | agent.row | panetitle = "Send via Slack"command = "slack.send_file"Extend mnml’s right-click menus with sibling-provided items. target picks which right-click surface the entry appears in; unknown targets are silently dropped at merge time.
[[menu_bar]] — menu-bar entries
Section titled “[[menu_bar]] — menu-bar entries”[[menu_bar]]path = "File > Send via Slack" # slash-separated menu pathcommand = "slack.send_file"Add rows to mnml’s menu bar. The path is a >-separated menu hierarchy; missing intermediate menus get created.
[statusline] — sibling-owned segment
Section titled “[statusline] — sibling-owned segment”[statusline]side = "right" # "left" | "right"segment_id = "slack" # unique id used to update / clear laterinitial_text = "◇ slack" # what renders on startupinitial_color = "comment" # named theme color, optionalclick_command = "slack.open" # optionalpriority = 100 # 100 default; 200 = "always show"min_width = 4 # drop segment below this widthmax_width = 30 # truncate content above thisReserves space in the statusline that the sibling can update at runtime via the Bridge SDK’s statusline_set_segment helper. Priority-based overflow: when statusline width is tight, low-priority segments drop first.
[notifications] — OS notification policy
Section titled “[notifications] — OS notification policy”[notifications]os_notify_on = "error_only" # never | error_only | alwaysos_rate_limit_sec = 5 # min secs between OS pingsControls whether the sibling’s notify calls escalate to OS notifications (OSC 9 / 777). error_only (the reasonable default for most siblings) fires only on Level::Error; always fires on every notify; never disables OS escalation entirely.
[requires] — preconditions
Section titled “[requires] — preconditions”[requires]env = ["SLACK_TOKEN"] # dim chip if any of these is unsetbinary = "mnml-msg-slack" # PATH-verified at discoveryIf any listed env var is missing, or the named binary isn’t on $PATH, the discovery overlay dims the row and the manifest’s is_ready() returns false. The chip still renders — this is a hint, not a hard gate.
[[settings]] — settings-overlay pages
Section titled “[[settings]] — settings-overlay pages”[[settings]]section = "Slack"label = "Channel filter"help = "Comma-separated list of channels to prioritize"Reserves rows in the mnml settings overlay. v1 is a metadata hook — the sibling handles actual value storage via ~/.config/mnml-msg-slack.toml or similar; mnml just surfaces the section header + label.
Template variables
Section titled “Template variables”Every run string is passed through launcher_template::expand before dispatch. Recognized {{name}} tokens get substituted; unrecognized tokens stay literal (so a typo like {{workspce}} reads as-typed at spawn time, easier to debug than a hard failure).
The full vocabulary:
| Token | Meaning | Empty when |
|---|---|---|
{{workspace}} |
Absolute path of the active workspace root | never |
{{workspace_name}} |
Basename of the workspace directory | never |
{{current_file}} |
Active file path, relative to workspace when possible | no editor pane focused |
{{current_file_abs}} |
Absolute path of the current file | no editor pane focused |
{{current_file_dir}} |
Directory of the current file (absolute) | no editor pane focused |
{{cursor_line}} |
1-indexed cursor line | no editor pane focused |
{{cursor_col}} |
1-indexed cursor column | no editor pane focused |
{{selection}} |
Selected text — single line only in v1 | no selection |
An empty value expands to an empty string, not a literal {{name}}. That means code {{current_file}} with no focused editor becomes code (a launch-with-no-file that most CLIs handle gracefully) — not code {{current_file}} (which would fail to parse).
Not yet implemented: {{prompt:<name>}} — reserved for the launcher-edit overlay’s prompt-at-spawn work. {{prompt:target}} in a run string stays literal for now; adding a prompt-at-spawn substitution requires plumbing through the async prompt subsystem.
Worked examples
Section titled “Worked examples”Open the current file’s directory in lazygit:
[[commands]]id = "lazygit.here"title = "lazygit: open at current file's dir"run = ":term lazygit -w {{current_file_dir}}"Jump to the current position in an external editor:
[[commands]]id = "vscode.here"title = "VS Code: open at cursor"run = ":term code -g {{current_file_abs}}:{{cursor_line}}:{{cursor_col}}"Grep the workspace for the current selection:
[[commands]]id = "rg.selection"title = "ripgrep: search workspace for selection"run = ":term rg -n {{selection}} {{workspace}}"Workspace-specific Claude launcher (see Absorbed launcher pattern below):
[[commands]]id = "claude.custom"title = "Claude: launch with workspace config"keys = ["<leader>ac"]run = ":term claude --dangerously-skip-permissions --project {{workspace}}"Add a local launcher in-app
Section titled “Add a local launcher in-app”For a private launcher you don’t want to share via the marketplace, run:
:launcher.add_localThe palette command opens the integration edit overlay (AddCustom mode) with an empty :term command pre-seeded. Fill in id / label / glyph / fallback / color / command, hit Save, and mnml writes a full <id>.toml authorial manifest to ~/.config/mnml/integrations/. The chip appears immediately — no integrations.refresh needed.
The full flow:
:launcher.add_local(or bind it to a key via[keys.global]).- Type in the fields.
Tabmoves between them;←→cycles the color palette. - Save. mnml writes the file and updates the in-memory rail.
For anything more permanent than a one-off — a launcher you want in every workspace, or that you’d like others to install — the in-app overlay is still fine, but you can also write the manifest by hand. See Integrations overview → three paths for the tradeoffs.
Absorbed launcher pattern
Section titled “Absorbed launcher pattern”The old per-workspace Claude launcher override — a [workspace] claude_launcher config key that mnml used to shell to when <leader>ac fired inside a specific project — was retired when the launcher template shipped in 2026-08. The template model absorbs the same use case cleanly:
Before (dead — do not use):
# ~/.mnml/config.toml, per-workspace[workspace]claude_launcher = "claude --dangerously-skip-permissions --project /Users/me/proj"Now — a workspace-local launcher manifest:
# <workspace>/.mnml/integrations/claude-custom.tomlid = "claude-custom"label = "Claude (this project)"category = "ai"
[chip]glyph = "\u{F0668}"fallback = "Cc"color = "orange"
[[commands]]id = "claude.custom"title = "Claude Code: launch with project config"keys = ["<leader>ac"]run = ":term claude --dangerously-skip-permissions --project {{workspace}}"Same behavior, and the manifest is portable — copy the file into another workspace’s .mnml/integrations/ folder and the chord + chip come with it. {{workspace}} expands per workspace, so one manifest can be shared across many projects.
If you want the launcher to only appear in one workspace, drop the manifest in <workspace>/.mnml/integrations/ (workspace-local). To share it across every project, put it in ~/.config/mnml/integrations/ (user-global). Same file, different location.
Merging with mnml core defaults
Section titled “Merging with mnml core defaults”Three chips are hardcoded in mnml core: browser, claude_code, codex. When a manifest’s id matches one of them, the manifest replaces the built-in as the effective base — mnml’s default chip becomes just a fallback for the case where no manifest exists.
If a user hits Edit… on one of the three built-ins and no manifest exists for that id yet, the save promotes to a full authored <id>.toml in ~/.config/mnml/integrations/. Subsequent edits write <id>.override.toml sidecars over that promoted base. See Installing → promotion when there’s no base for the exact behavior.
- Installing integrations — Marketplace tab, sidecar overrides, hand-editing config.
- Marketplace — how the Marketplace tab discovers launchers to install.
- Integrations overview — the two flavors, one on-disk shape.
- Building integrations — authoring a launcher or (rarely) a binary sibling.
- Bridge & Mount — the runtime protocol siblings use once they need to talk back.