"Icons" in Finch are actually two independent systems, tied to two different surfaces. It's easy to conflate them when building a mini tool, so let's separate them first:
Entry icon (icon.png) | UI icon (IconRef) | |
|---|---|---|
| Shown where | Toolbox list / detail page / community recommendation card | Composer toolbar button, button dropdown items |
| Represents | The mini tool itself | A specific interaction point (a button, a menu item) |
| Format | A PNG file | A string reference |
| Provided by | A file in the package root | Manifest declaration + code registering SVGs on demand |
1. Entry icon (icon.png)
Spec
- The filename must be exactly
icon.png(case-sensitive), placed in the package root next topackage.json. - Must be PNG format.
- The community catalog requires square, 128×128 to 300×300 pixels (inclusive). Local private installs aren't size-checked, but to stay crisp across display sizes (a 40px small icon / a 64px card image), aim for this spec anyway.
- Prefer a transparent or solid background — avoid gradients or photo-like content covering the whole image; the icon ends up displayed around 40px, where detail gets lost.
Two loading paths
- Installed: the Finch main process reads
<extension-dir>/icon.pngdirectly from disk, served to the renderer through the internalfinch-ext-icon://protocol. It only recognizes this one file in the package root — no subdirectories, no other filenames. - Not installed (shown in the community catalog): Finch constructs
https://unpkg.com/<npm-package>@<version>/icon.pngdirectly — which meansicon.pngmust actually be included in the npm-published tarball. Confirm withnpm pack --dry-runbefore publishing; if it only exists in your git repo and wasn't included innpm publish, the community card falls back to a default placeholder. - With no icon at all, the fallback is a neutral square icon (
lucide-react'sBlocks) — no error, just a less polished experience.
2. UI icon (IconRef)
Composer buttons and their dropdown menu items reference icons via a string (similar to VS Code's ThemeIcon), rather than embedding a component or image directly. This string is called IconRef, with three forms:
| Form | Meaning |
|---|---|
"settings" or "Settings" | A built-in Finch Lucide icon name — kebab-case or PascalCase both resolve |
"lucide:settings" | Same as above, with an explicit prefix (recommended) |
"ext:<packId>/<iconId>" | References an icon from an SVG icon pack you registered at runtime |
Used when declaring a button slot in the manifest:
"contributes": {
"composerActions": [
{ "id": "my-btn", "icon": "GitBranch", "tooltip": "..." }
]
}
Code can also return an IconRef dynamically via getIcon() or a menu item's iconName, overriding the static manifest declaration.
The built-in Lucide set is a fixed allowlist
It's not the entire Lucide icon library — Finch maintains a central registry (BUILTIN_ICONS), and only names on it resolve to an icon. Anything else is treated as plain text (i.e. an unregistered icon name renders as that literal string, not an error and not some Lucide icon). The current registry leans toward common linear UI icons, e.g.:
settings · star · sparkles · zap · timer · filter · list
git-branch · git-commit-horizontal · clipboard · clipboard-check · clipboard-list
file · file-text · message-circle · log-in · puzzle · check
toggle-left · toggle-right · wand-sparkles · zoom-in · zoom-out
folder · hash · rocket · shield · cloud · calendar · users · bookmark …
This list keeps growing — don't guess or assume a name outside it works. Before integrating, run your button in Finch to confirm the icon actually renders, rather than degrading into text. If the icon you need genuinely isn't in the built-in set, register a runtime icon pack instead.
When the built-in set doesn't have what you need: register a runtime SVG icon pack
Two steps, the same "manifest static declaration + code dynamic registration" pattern as ComposerAction buttons:
1. Declare the icon pack namespace in the manifest (just declares that the pack exists — no actual graphics here):
"contributes": {
"iconPacks": [{ "id": "my-icons", "label": "My Icons" }]
}
2. Register the actual SVGs in activate():
ctx.subscriptions.push(
ctx.icons.register('my-icons', {
rocket: { svg: '<svg viewBox="0 0 24 24">...</svg>' },
}),
);
Once registered, reference it as ext:my-icons/rocket; if you're referencing your own icon pack from inside the same extension, you can also shorten it to ext:rocket.
Hard constraints when designing SVGs
For security, Finch treats an icon strictly as "a pure vector image" — no scripts, animations, or external images/links are supported. Anything that doesn't comply is silently dropped at render time, with no error shown. Follow these rules so an icon renders correctly the first time you register it:
- Use basic vector shapes only: paths, circles, rectangles, polygons — no
<script>,<image>, animation tags, or references to external images/links (includinghttp(s)://URLs). - Don't hardcode colors. Just fill/stroke normally — Finch automatically makes the icon follow the current theme (light/dark), so there's no need to handle color yourself.
- Use a
24×24canvas. You don't need to worry about specific pixel units — the icon scales with the current font size automatically, as long as your artwork is proportioned for a 24×24 viewBox. - Keep the file small. A simple linear icon should be a few KB at most — that's never a problem.
Actual rendering size
Composer toolbar button icons and dropdown menu item icons currently render at 14px, strokeWidth: 1.8, mixed inline with Finch's built-in Lucide icons. To visually match the built-ins with a custom SVG icon pack:
- Use a 24×24 canvas, with the main artwork occupying roughly an 18–20px visible area (matching Lucide's default padding).
- Match stroke weight to Lucide's
strokeWidth: 1.8–2— too thick or thin looks inconsistent with other icons at 14px. - Prefer a stroke style (
fill="none"+stroke="currentColor") — large filled blocks tend to blur together at 14px.
3. Best practices
- Prefer the built-in Lucide set — don't build a custom icon pack for every mini tool just for looks. The built-in set already covers most Composer scenarios (settings, filters, files, git, toggles...); only register a runtime icon pack when there's genuinely no matching semantic in the built-in set.
icon.pngshould be recognizable at 40px — avoid complex gradients, text, or photos; simple monochrome/duotone artwork works best.- Don't hardcode colors in an SVG, unless the icon genuinely needs multi-color semantics (like a status light) — otherwise let
currentColorinherit the theme color. - Register icon packs on demand, not preemptively — every icon registered via
ctx.icons.register()gets sanitized and cached, so only register what you actually use. - Use kebab-case naming: keep
packIdandiconIdconsistent with your mini tool's ownidstyle, for easier debugging and reuse.
Checklist
[ ] icon.png is in the package root, filename matches exactly (case-sensitive)
[ ] icon.png is PNG, square, 128×128–300×300 pixels
[ ] npm pack --dry-run confirms icon.png is in the publish manifest
[ ] Composer button / menu icon names are checked against the built-in Lucide allowlist first, to avoid falling back to text
[ ] Custom SVG icon packs follow the 24×24 canvas, currentColor, no-external-references constraints
[ ] Custom SVG icon stroke weight visually matches Lucide (strokeWidth ~1.8–2)