← Docs
Helix CLI docs
Browse Helix CLI docs

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 when HELIX_STUDIO_USER_PLUGINS=1)
  • Any additional directories in HELIX_STUDIO_PLUGIN_PATHS (separated by os.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.json
  • HASHES.json (sha256 for every file in the package)

Optional:

  • code/** (Python code; preferred) or legacy top-level code files
  • assets/** (web panel assets)
  • wheels/** (vendored wheels for dependency isolation in dedicated extension hosts)
  • SIGNATURE.ed25519 + PUBLISHER.pub (Ed25519 signature over HASHES.json)

Build determinism rules:

  • Zip entries are sorted.
  • Zip timestamps are fixed.
  • HASHES.json is 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=1 blocks unsigned packages.
  • HELIX_STUDIO_PLUGIN_REQUIRE_TRUSTED=1 blocks 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_world
  • name: UI label
  • version: plugin version string
  • plugin_api (or legacy api_version): required Helix plugin API major version (currently "1")
  • entry: module path or module + callable (see below)

Optional fields:

  • description
  • author
  • publisher (publisher id; required for signing/trust workflows)
  • helix_min_version / helix_max_version (optional compatibility gates; see semantics below)
  • isolation (in_process or extension_host)
  • extension_host_mode (shared or dedicated; only used when isolation: "extension_host")
  • permissions (enforced for isolation: "extension_host" at the host API boundary + web panel sandbox)

entry formats:

  • "my_plugin_module" → calls my_plugin_module.activate(host_api)
  • "my_plugin_module:activate" → calls activate(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 as deactivate()
  • define deactivate() or deactivate(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() -> bool
  • host_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 Shift during 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=0 disables all plugin loading.
  • HELIX_STUDIO_DISABLED_PLUGINS=id1,id2 disables specific plugins.
  • HELIX_STUDIO_USER_PLUGINS=1 enables discovery from ~/.helix/plugins.
  • HELIX_STUDIO_SAFE_MODE=1 starts with plugins disabled for this session.
  • HELIX_STUDIO_EXTENSION_HOST=1 enables loading plugins with isolation: "extension_host".
  • HELIX_STUDIO_EXTENSION_HOST_MODE=shared|per_plugin controls 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.0
  • HELIX_STUDIO_EXTENSION_HOST_HEARTBEAT_TIMEOUT_S=5.0
  • HELIX_STUDIO_EXTENSION_HOST_HEARTBEAT_MISSES_BEFORE_RESTART=2
  • HELIX_STUDIO_EXTENSION_HOST_MAX_RSS_MB=1024
  • HELIX_STUDIO_EXTENSION_HOST_MAX_PENDING=200
  • HELIX_STUDIO_EXTENSION_HOST_MAX_DROPPED_NOTIFICATIONS=500
  • HELIX_STUDIO_PLUGIN_POLICY_VIOLATION_QUARANTINE=25
  • HELIX_STUDIO_EXTENSION_HOST_TIMEOUT_QUARANTINE=3
  • HELIX_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 QWidget support 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" in helix_plugin.json
    • Safe-by-default: when no extension_host_mode override is provided, Studio may choose a dedicated host for higher-risk permission sets (e.g. subprocess/network/web panel external nav).
  • Command handlers may optionally accept a ctx argument (cooperative cancellation):
    • ctx.is_cancelled() -> bool
    • ctx.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_html is 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 contains entry_html.

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 via HelixWebPanel.response(requestId, result, error)
  • HelixWebPanel.cancel(requestId) (best-effort; cooperative cancel in the extension host)
  • HelixWebPanel.message(payload) (signal; sent by host_api.ui.post_message(...))

Your entry_html should include:

  • qrc:///qtwebchannel/qwebchannel.js
  • A QWebChannel setup that assigns channel.objects.HelixWebPanel to 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 (default false)
  • permissions.system (or legacy top-level shortcuts open_url / clipboard):
    • open_url: true|false (default false)
    • clipboard: true|false (default false)
  • permissions.web_panels (optional overrides):
    • enabled: true|false
    • network: same format as above
    • allow_external_navigation: true|false (default false)
    • allow_popups: true|false (default false)
    • allow_downloads: true|false (default false)
    • allow_cookies: true|false (default false)
    • allow_persistent_storage: true|false (default false)
    • allow_insecure_http: true|false (default false)
    • allow_websocket: true|false (default false)
    • csp_enabled: true|false (default true)
    • csp_allow_inline_script: true|false (default false)
    • csp_allow_eval: true|false (default false)
    • csp_allow_remote_scripts: true|false (default false)

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 by permissions.network)
  • host_api.files.read_text(path) / write_text(path, text) (gated by permissions.file)
  • host_api.subprocess.run(args) (gated by permissions.subprocess)
  • host_api.system.open_url(url) / host_api.system.clipboard_set(text) (gated by permissions.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_version is inclusive (plugin loads when helix_version >= helix_min_version)
  • helix_max_version is inclusive (plugin loads when helix_version <= helix_max_version)
  • Comparisons use a base version (major.minor.patch) extracted from the Helix build version, so versions like 1.0.0+git, 1.0.0-123-gabc, or 1.0.0.post4 are treated as base 1.0.0 for compatibility checks.
  • Values are trimmed and may start with a leading v (e.g. v1.2.3). Use 1, 1.2, or 1.2.3.