This guide is the canonical engineering standard for HaloForge plugins. It covers the plugin shape, Rust backend contract, React panel contract, host SDK usage, local test flow, release flow, and the UX rules official plugins must follow.
For the hosted documentation site, use https://docs.haloforge.dev/plugins.
HaloForge plugins should feel like part of HaloForge, not like embedded websites. A plugin can be authored outside the main app, but it must use the public SDK, host theme tokens, host controls, scoped CSS, and explicit permissions.
The plugin host should remain a black box:
- Do not read
window.__HF_HOSTor other private globals. - Do not call private Tauri commands directly when an SDK helper exists.
- Do not construct
plugin_invokewire command names by hand; useinvokePlugin()orinvokeOtherPlugin(). - Do not assume enterprise-only services are present in Community Edition. Provide a local or custom endpoint path when the feature can work without a managed HaloForge service.
| Level | Use case | Typical registration |
|---|---|---|
| 0 | Top-level module in the sidebar | defineModulePlugin() or definePlugin({ panel }) |
| 1 | Feature inside an existing module | definePlugin({ panel }) with level 1 manifest config |
| 2 | UI slot injection | definePlugin({ slots }) |
| 3 | Assistant registration | defineAssistantPlugin() |
| 4 | Headless service or workflow backend | Rust backend commands and manifest level 4 config |
Pick the smallest level that fits the product surface. A full workspace such as Image Studio should be Level 0; a toolbar button should be Level 2.
Use templates/level0-rust-react as the starting point for official UI plugins.
my-plugin/
manifest.json
backend/
Cargo.toml
src/lib.rs
app/
package.json
src/index.tsx
src/Panel.tsx
src/styles.css
assets/
dist/
This keeps native code, frontend code, packaged assets, and release output separated. The packer can build both sides and emit a signed .hfpkg.
Every plugin needs a manifest.json at the repository root.
{
"id": "dev.example.my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"description": "Short user-facing summary.",
"author": "Example",
"compatibility": {
"min_app_version": "0.8.0",
"min_host_api_version": "0.2.15"
},
"capability_levels": [0],
"host_capabilities": ["navigation", "file_intents", "theme_read"],
"integration": {
"level0": {
"module_id": "my-plugin",
"module_label": "My Plugin",
"module_icon": "Sparkles",
"sidebar_position": "main",
"sidebar_order": 120,
"panel_entry": "app/dist/index.js"
}
},
"window": {
"default_open_mode": "reuse_or_new",
"reuse_key": "resource",
"allow_multiple": true,
"document_handlers": [
{
"id": "markdown",
"label": "Markdown",
"extensions": [".md", ".markdown"],
"mime_types": ["text/markdown"],
"route": "/document",
"resource_param": "path"
}
]
},
"entry": {
"native": {
"macos_arm64": "backend/target/release/libmy_plugin.dylib",
"macos_x64": "backend/target/release/libmy_plugin.dylib",
"windows_x64": "backend/target/release/my_plugin.dll",
"linux_x64": "backend/target/release/libmy_plugin.so"
},
"frontend": "app/dist/index.js",
"frontend_styles": "app/dist/styles.css"
},
"permissions": [
{ "type": "ipc_register" },
{ "type": "host_theme_read" }
],
"commands": [
{ "id": "ping", "description": "Return plugin health information." }
]
}Rules:
idis stable forever. Do not rename it after publishing.versionfollows semver. Official plugins should start at0.1.0.host_capabilitiesmust match SDK helpers used by the frontend.permissionsmust be the smallest set needed.entry.frontendandintegration.*.panel_entryshould point to the built bundle.- Native command IDs in
commandsmust match the names registered by the Rust backend before SDK prefixing.
Plugins can declare a window block when their routes or resources should participate in HaloForge's multi-window dispatcher.
default_open_mode:smart,current,new_window,reuse_existing, orreuse_or_new.reuse_key:plugin,route,resource, ornone.allow_multiple: whether the plugin can have multiple windows at once.document_handlers: file/resource handlers that map extensions or MIME types into plugin routes.
The host still owns actual window creation, focus, session restore, and safety checks. The plugin declares intent; HaloForge decides the final target window.
document_handlers are the supported plugin-owned "open with" capability. Host menu actions can still be source-specific: File > Open Markdown is a current-window navigation action, while OS file activation and deep links may use the plugin's multi-window policy.
The app menu bar is host-owned. The current Host API does not expose arbitrary plugin menu injection; add a documented manifest/SDK contribution before relying on plugin-provided File/Edit/View menu items.
Use a Rust backend whenever the plugin needs:
- native file system access
- outbound HTTP without relying on browser CORS
- secret handling
- local process integration
- long-running or host-owned tasks
- privileged operations that need manifest permissions
Frontend-only plugins are acceptable for pure UI surfaces, simple slot renderers, or panels that only consume public host SDK hooks. Official plugins that call external APIs directly should normally put those calls behind Rust commands.
Minimal backend:
use haloforge_plugin_api::*;
pub struct MyPlugin;
impl MyPlugin {
pub fn new() -> Self { Self }
}
impl HaloForgePlugin for MyPlugin {
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
id: "dev.example.my-plugin".into(),
name: "My Plugin".into(),
version: "0.1.0".into(),
description: "A sample HaloForge plugin".into(),
author: "Example".into(),
abi_version: PLUGIN_ABI_VERSION,
}
}
fn on_load(
&mut self,
_ctx: &dyn PluginContext,
ipc: &mut dyn IpcRegistrar,
) -> Result<(), PluginError> {
ipc.register("ping", Box::new(|args, _ctx| {
Ok(serde_json::json!({
"ok": true,
"echo": args
}))
}))?;
Ok(())
}
}
declare_plugin!(MyPlugin, MyPlugin::new);Register the plugin bundle once:
import { definePlugin, registerPlugin } from "@haloforge/plugin-sdk";
import { MyPanel } from "./Panel";
export default registerPlugin("dev.example.my-plugin", definePlugin({
panel: MyPanel,
}));Use the SDK for host integration:
invokePlugin()for this plugin's Rust commands.invokeOtherPlugin()for declared plugin-to-plugin dependencies.useHostTheme()oruseAppTheme()for theme tokens.usePluginSettings()for host-provided plugin settings.useHostAI()for host AI chat transport.enterpriseGateway()for the host-managed image gateway. The function name is retained for compatibility, but product UI should call this "HaloForge Cloud gateway" or "managed image gateway".usePluginNavigation()for Level 0 plugin panels with internal pages. CallpushRoute()on page-level navigation and update local state fromcurrentoronRouteChange()so HaloForge Back/Forward can restore the plugin page.usePluginWindows()when a plugin wants HaloForge to open one of its routes or resources in the best host window according to the manifestwindowpolicy.AppSelectfor combo boxes and dropdowns.pickHostFile(),pickHostDirectory(), andsaveHostFile()for host-owned file dialogs.
Do not build controls with raw HTML selects if the SDK has an equivalent host control.
usePluginNavigation() and usePluginWindows() solve different problems:
import { usePluginNavigation, usePluginWindows } from "@haloforge/plugin-sdk";
function Panel() {
const navigation = usePluginNavigation();
const windows = usePluginWindows();
function openLocalDetail(id: string) {
navigation.pushRoute(`/detail/${id}`, { params: { id } });
}
async function openDocument(path: string) {
await windows.openResource(path, {
route: "/document",
params: { path },
reuseKey: "resource",
openMode: "reuse_or_new",
});
}
}Use navigation for current-window history. Use windows for route/resource handoff to the host multi-window dispatcher. Plugins should not call private Tauri commands to create windows.
Plugins that show a specific document or task can update the native window title:
import { usePluginWindowTitle } from "@haloforge/plugin-sdk";
usePluginWindowTitle(fileName, { subtitle: "Markdown" });HaloForge only accepts title updates from the plugin that owns the active plugin module or route, so background panels cannot overwrite another plugin's title.
Official plugins must adapt to HaloForge's shell:
- Scope CSS under a plugin root class such as
.hfmy-root. - Do not style
body,html, or global element selectors outside the plugin root. - Use HaloForge CSS variables first:
--color-background,--color-surface,--color-foreground,--color-foreground-secondary,--color-border,--color-primary, and shell variables such as--hf-shell-bg. - Support light and dark themes without hard-coding a single palette.
- Use
AppSelectfor dropdowns, host file pickers for file selection, and lucide icons for icon buttons. - Keep cards at 8px radius or less unless the host surface already uses another radius.
- Do not put page sections inside nested cards.
- Do not add visible instructional copy that explains basic UI mechanics.
- Keep text responsive. Labels, buttons, chips, and cards must not overflow at narrow widths.
Plugins should ship English and Chinese strings for all user-visible UI text.
Recommended pattern:
- Create a typed translation key map.
- Read
localStorage["hf:locale"]when available. - Fall back to
navigator.language. - Fall back to English for missing keys.
- Keep provider names and API terms stable, but localize labels, buttons, status, errors, and empty states.
Use neutral product language in Community Edition. For example, show "HaloForge Cloud gateway" or "Managed gateway", not "Enterprise gateway", unless the UI surface is explicitly enterprise-only.
Image generation plugins should support two paths:
- Managed gateway: call
enterpriseGateway()from@haloforge/plugin-sdk, requesthost_capabilities: ["enterprise_gateway"], and declare{ "type": "host_enterprise_gateway_access" }. - Custom endpoint: let Community Edition users configure an OpenAI-compatible
baseUrland optional API key. Route HTTP through the Rust backend when CORS or secret handling matters.
The managed gateway may be backed by HaloForge Cloud or Enterprise Server depending on the signed-in host. Plugin UI should not make that distinction unless it directly affects the user's action.
Use the same checks before every handoff:
npm install
npm run typecheck
npm run build
cargo fmt --check
cargo check
npx @haloforge/plugin-pack check .
npx @haloforge/plugin-pack pack . --out dist/packageInstall the package into a local HaloForge checkout:
cd /path/to/HaloForge
npm run hf -- plugin install local /path/to/my-plugin/dist/package/dev.example.my-plugin-0.1.0.hfpkg --jsonThen launch HaloForge and verify:
- the plugin panel mounts
- the bundle registers the same plugin ID as
manifest.json - light and dark themes look native
- SDK controls render correctly
- Rust commands return expected data
- expected permissions appear in the install prompt
- logs have no panel registration, IPC, or permission errors
For official plugins:
npx @haloforge/plugin-pack check .
npx @haloforge/plugin-pack pack . --release --out dist/package
npx @haloforge/plugin-pack metadata dist/package/dev.example.my-plugin-0.1.0.hfpkg \
--signing-key-id haloforge-official-2026-05 \
--signing-key-env HF_PLUGIN_SIGNING_PRIVATE_KEY \
--pretty \
--output dist/catalog-draft.jsonOfficial repositories should release from GitHub Actions using repository secrets:
hf_plugin_signing_private_keyhf_plugin_signing_key_idHF_ADMIN_TOKENwhen the workflow submits catalog metadata
Publish SDK and packager releases before publishing plugins that require new SDK or permission support.
- Manifest ID, version, entries, host capabilities, permissions, and commands match implementation.
- Frontend registers exactly once with the manifest plugin ID.
- No direct private host bridge usage.
- No direct
plugin_invokewire names. - UI uses host tokens and SDK controls.
- English and Chinese strings cover all visible text.
- Community Edition path works without Enterprise Server.
- Rust backend handles external HTTP and secrets when needed.
hf-pack check, frontend build, Rust check, and local install pass.