Helix Studio plugins (Layer 1: in-process)
Helix Studio plugins run arbitrary Python code inside the Studio UI process. Only install plugins you trust.
This document describes Layer 1: Python packages loaded in-process. The public API is intentionally small and stable.
Note: the manifest supports isolation: "extension_host" for the planned Layer 2
out-of-process extension host (for dependency/safety isolation).
Install locations (discovery)
Helix Studio scans for helix_plugin.json in:
<project-root>/plugins/**/helix_plugin.json~/.helix/plugins/**/helix_plugin.json(only whenHELIX_STUDIO_USER_PLUGINS=1)- Any additional directories in
HELIX_STUDIO_PLUGIN_PATHS(separated byos.pathsep)
HELIX_STUDIO_PLUGIN_PATHS examples:
- macOS/Linux:
/a/plugins:/b/plugins - Windows:
C:\a\plugins;D:\b\plugins
Distribution: .helixplugin packages
Helix Studio supports installing plugins from deterministic, signed packages:
*.helixplugin (a zip file under the hood).
Package contents
Required:
helix_plugin.jsonHASHES.json(sha256 for every file in the package)
Optional:
code/**(Python code; preferred) or legacy top-level code filesassets/**(web panel assets)wheels/**(vendored wheels for dependency isolation in dedicated extension hosts)SIGNATURE.ed25519+PUBLISHER.pub(Ed25519 signature overHASHES.json)
Build determinism rules:
- Zip entries are sorted.
- Zip timestamps are fixed.
HASHES.jsonis canonical JSON (stable ordering).
Signature policy and trust
Signing is keyed by the manifest publisher string.
Studio maintains a local trust store at:
~/.helix/plugins/trust_store.json
Install-time policy can be enforced via env vars:
HELIX_STUDIO_PLUGIN_REQUIRE_SIGNED=1blocks unsigned packages.HELIX_STUDIO_PLUGIN_REQUIRE_TRUSTED=1blocks publisher keys not present in the trust store.
Important: signature/trust gates whether Studio will accept a package; it is not an OS sandbox.
Install / update / rollback UX
In Tools → Plugins…:
- Install from file… installs a local
*.helixplugin. - Install from URL… downloads and installs a
*.helixplugin(optionally with an expected sha256). - Check updates… loads a registry index and annotates plugins with available updates.
- Update selected installs the latest release from the registry for that plugin.
- Rollback… re-installs a cached previous version.
- Pin version disables updates for that plugin until unpinned.
Packages are cached locally under the chosen install root:
<plugins_dir>/.helix_pkg_cache/<plugin_id>/<version>/<plugin_id>-<version>.helixplugin
Minimal registry index format
Studio can consume a simple JSON registry index (hosted anywhere, e.g. GitHub Releases):
{
"format": 1,
"plugins": [
{
"plugin_id": "acme.demo",
"name": "Demo",
"versions": [
{
"version": "0.2.0",
"url": "https://…/acme.demo-0.2.0.helixplugin",
"sha256": "…",
"publisher": "acme",
"capabilities": {"network": "none"}
}
]
}
]
}
You can point Studio at a registry URL with:
HELIX_STUDIO_PLUGIN_REGISTRY_URL=https://…/index.json
Manifest (helix_plugin.json)
Required fields:
id(lowercase, stable): e.g.hello_worldname: UI labelversion: plugin version stringplugin_api(or legacyapi_version): required Helix plugin API major version (currently"1")entry: module path or module + callable (see below)
Optional fields:
descriptionauthorpublisher(publisher id; required for signing/trust workflows)helix_min_version/helix_max_version(optional compatibility gates; see semantics below)isolation(in_processorextension_host)extension_host_mode(sharedordedicated; only used whenisolation: "extension_host")permissions(enforced forisolation: "extension_host"at the host API boundary + web panel sandbox)
entry formats:
"my_plugin_module"→ callsmy_plugin_module.activate(host_api)"my_plugin_module:activate"→ callsactivate(host_api)
Supported layouts:
<plugin-root>/<module>.py<plugin-root>/src/<module>.py(recommended for packaging)
Lifecycle
activate(host_api)
Called once per load. A plugin should register everything through host_api.
deactivate(...) (optional)
Used for cleanup (stop timers/threads, close sockets, etc).
Accepted forms:
- return a callable from
activate(...)→ treated asdeactivate() - define
deactivate()ordeactivate(host_api)in the entry module - return an object with
.deactivate()/.deactivate(host_api)
On soft reload, Helix calls deactivate hooks best-effort and then removes everything registered via the host registries.
Stable host API surface (v1)
host_api.plugin_id
host_api.log(message) (writes to plugin diagnostics + Studio log)
Settings (persistent)
host_api.settings.get(key, default=None)
host_api.settings.set(key, value)
host_api.settings.remove(key)
Values must be JSON-serializable.
Paths (per-plugin directories)
host_api.paths.user_data_dir() -> Path
host_api.paths.cache_dir() -> Path
host_api.paths.temp_dir() -> Path
Commands
host_api.commands.register(id, title, callback, *, category=None, shortcut=None, menu=None)
Rules:
- Command ids must be namespaced:
"{plugin_id}.*"(no overrides). - Shortcuts and menu entries created via the host are tracked and removed on unload.
UI
host_api.ui.register_dock_panel(id, title, widget_factory, *, area="bottom", visible=False, menu="View/Plugin Panels")
Rules:
- Dock panel ids must be namespaced:
"{plugin_id}.*".
Thread helpers:
host_api.ui.is_main_thread() -> boolhost_api.ui.run_on_main_thread(fn)(queues onto the UI thread)
Events
host_api.events.subscribe(topic, handler) -> token
host_api.events.emit(topic, payload)
host_api.events.unsubscribe(token) -> bool
Recommended topic naming:
- Core:
core.project.opened,core.run.recorded - UI:
ui.selection.changed - Plugin-owned:
{plugin_id}.*
Reload semantics (important)
Tools → Plugins → Reload plugins (soft) is a soft reload:
- It does not guarantee updated module code (Python keeps imported modules).
- It does guarantee Helix un-registers all commands/docks created via the host.
For “real” reload (fresh interpreter), use:
Tools → Plugins → Restart Studio (clean reload)
Safe mode
- Hold
Shiftduring startup to be offered Safe Mode (plugins disabled for this session). - If the last launch crashed during plugin load, Studio will offer Safe Mode automatically.
Disable / safe mode switches
HELIX_STUDIO_PLUGINS=0disables all plugin loading.HELIX_STUDIO_DISABLED_PLUGINS=id1,id2disables specific plugins.HELIX_STUDIO_USER_PLUGINS=1enables discovery from~/.helix/plugins.HELIX_STUDIO_SAFE_MODE=1starts with plugins disabled for this session.HELIX_STUDIO_EXTENSION_HOST=1enables loading plugins withisolation: "extension_host".HELIX_STUDIO_EXTENSION_HOST_MODE=shared|per_plugincontrols whether remote plugins share one host process or run per-plugin.
Auto quarantine (crash loops / policy violations)
Studio may auto-disable (“quarantine”) a plugin if it repeatedly crashes its dedicated extension host, times out remote commands, or triggers too many policy violations (e.g. web panel sandbox). Quarantined plugins show in the Plugins dialog with a reason; re-enable a plugin to clear quarantine and try again.
Defaults (override via env vars):
HELIX_STUDIO_EXTENSION_HOST_HEARTBEAT_INTERVAL_S=2.0HELIX_STUDIO_EXTENSION_HOST_HEARTBEAT_TIMEOUT_S=5.0HELIX_STUDIO_EXTENSION_HOST_HEARTBEAT_MISSES_BEFORE_RESTART=2HELIX_STUDIO_EXTENSION_HOST_MAX_RSS_MB=1024HELIX_STUDIO_EXTENSION_HOST_MAX_PENDING=200HELIX_STUDIO_EXTENSION_HOST_MAX_DROPPED_NOTIFICATIONS=500HELIX_STUDIO_PLUGIN_POLICY_VIOLATION_QUARANTINE=25HELIX_STUDIO_EXTENSION_HOST_TIMEOUT_QUARANTINE=3HELIX_STUDIO_EXTENSION_HOST_CRASH_QUARANTINE=3
Extension host notes (Layer 2, experimental)
For isolation: "extension_host" plugins:
- UI objects still live in the main process (no remote
QWidgetsupport yet). - Capabilities are enforced at the host API boundary (and web panel sandbox), not as a full OS sandbox. A remote plugin can still use Python stdlib networking/file APIs unless the host process is additionally contained.
- Host process model:
- Default:
HELIX_STUDIO_EXTENSION_HOST_MODE=shared(all remote plugins share one host) - Isolation:
HELIX_STUDIO_EXTENSION_HOST_MODE=per_plugin(one host process per plugin) - Per-plugin override:
extension_host_mode: "shared"|"dedicated"inhelix_plugin.json - Safe-by-default: when no
extension_host_modeoverride is provided, Studio may choose a dedicated host for higher-risk permission sets (e.g. subprocess/network/web panel external nav).
- Default:
- Command handlers may optionally accept a
ctxargument (cooperative cancellation):ctx.is_cancelled() -> boolctx.throw_if_cancelled()ctx.progress(*, percent=None, stage=None, message=None)(reports progress to Studio)
Web panels (remote UI, Layer 2)
Remote plugins can contribute web-backed dock panels that run in the main process via Qt WebEngine.
host_api.ui.register_web_panel(id, title, entry_html, *, area="bottom", visible=False, menu="View/Plugin Panels", handler=None)
- Panel ids must be namespaced:
"{plugin_id}.*". entry_htmlis a path relative to the plugin root, e.g.ui/index.html.- Studio enforces a strict asset policy for web panels:
- Network requests are blocked (
http(s),ws(s)). file://requests are allowed only within the directory that containsentry_html.
- Network requests are blocked (
host_api.ui.post_message(id, payload)
- Sends a push message to the web panel (delivered to JS via the bridge below).
JS bridge
Studio exposes a Qt WebChannel object named HelixWebPanel to the page:
HelixWebPanel.call(requestId, method, params)→ responds viaHelixWebPanel.response(requestId, result, error)HelixWebPanel.cancel(requestId)(best-effort; cooperative cancel in the extension host)HelixWebPanel.message(payload)(signal; sent byhost_api.ui.post_message(...))
Your entry_html should include:
qrc:///qtwebchannel/qwebchannel.js- A
QWebChannelsetup that assignschannel.objects.HelixWebPanelto a global you can call.
Web panel policy (capabilities)
Web panel policy is derived from your manifest permissions:
permissions.network:"none"(default) /"full"/{"allow": ["example.com"]}permissions.file:"none"(default) /"read-only"/"read-write"permissions.subprocess:true|false(defaultfalse)permissions.system(or legacy top-level shortcutsopen_url/clipboard):open_url:true|false(defaultfalse)clipboard:true|false(defaultfalse)
permissions.web_panels(optional overrides):enabled:true|falsenetwork: same format as aboveallow_external_navigation:true|false(defaultfalse)allow_popups:true|false(defaultfalse)allow_downloads:true|false(defaultfalse)allow_cookies:true|false(defaultfalse)allow_persistent_storage:true|false(defaultfalse)allow_insecure_http:true|false(defaultfalse)allow_websocket:true|false(defaultfalse)csp_enabled:true|false(defaulttrue)csp_allow_inline_script:true|false(defaultfalse)csp_allow_eval:true|false(defaultfalse)csp_allow_remote_scripts:true|false(defaultfalse)
Studio injects a strict Content Security Policy (CSP) by default to reduce the attack surface for web panels.
Web panels run in an isolated Qt WebEngine profile by default:
- Off-the-record (no persistent disk storage)
- In-memory HTTP cache
- Cookies blocked unless
permissions.web_panels.allow_cookies: true - Persistent storage (disk) disabled unless
permissions.web_panels.allow_persistent_storage: true
Capability-gated helpers (Layer 2)
Remote plugins get a few extra helper surfaces that are gated by manifest permissions:
host_api.net.get_text(url)/host_api.net.get_json(url)(gated bypermissions.network)host_api.files.read_text(path)/write_text(path, text)(gated bypermissions.file)host_api.subprocess.run(args)(gated bypermissions.subprocess)host_api.system.open_url(url)/host_api.system.clipboard_set(text)(gated bypermissions.system)
These helpers are a sanctioned path for policy-checked IO; they don’t prevent a plugin from using Python stdlib APIs directly inside the extension host process.
Version gating semantics (Helix compatibility)
When helix_min_version / helix_max_version are present:
helix_min_versionis inclusive (plugin loads whenhelix_version >= helix_min_version)helix_max_versionis inclusive (plugin loads whenhelix_version <= helix_max_version)- Comparisons use a base version (
major.minor.patch) extracted from the Helix build version, so versions like1.0.0+git,1.0.0-123-gabc, or1.0.0.post4are treated as base1.0.0for compatibility checks. - Values are trimmed and may start with a leading
v(e.g.v1.2.3). Use1,1.2, or1.2.3.