// OPENBOX DOCS
Plugin API overview
Trust boundary and the local plugin lifecycle.
Plugins are optional local Python packages that observe or extend OpenBoxGL. They are untrusted-by-default third-party code: review a package before installing it, and use safe mode to disable all plugins when something misbehaves.
Trust boundary
- On Linux, each hook runs inside a bubblewrap OS sandbox when
bwrapis available: every namespace is unshared (no network access), the host root is mounted read-only, and/home,/tmp,/run,/mnt, and/mediaare replaced with empty filesystems. The plugin sees only its own package directory (read-only) and the JSON hook payload on stdin — never your data directory or home folder. - If the sandbox cannot be created, the plugin is skipped rather than run unsandboxed, unless you trust the plugin or set
OPENBOX_ALLOW_UNSANDBOXED_PLUGINS=1. Two paths exist: per-plugin "Trust and run" in the Plugins manager, which binds the grant to the installed package's SHA-256 (any update that changes the package invalidates the grant and re-prompts), or the legacy operator-wideOPENBOX_ALLOW_UNSANDBOXED_PLUGINS=1escape hatch for trusted local code. Reserve unsandboxed execution for plugin code you have read and audited: without the sandbox, the plugin runs as a plain child process with your user privileges and can read and modify files in your data directory and under your account, not only library entries. On Windowsbwrapdoes not exist, so plugins are skipped with a warning until trusted one by one in the Plugins manager or the variable is set. - In both modes the plugin environment is scrubbed before launch:
PYTHONPATH,PYTHONHOME,LD_PRELOAD, andLD_LIBRARY_PATHare removed,PYTHONNOUSERSITE=1is set, and any variable whose name containsTOKEN,PASSWORD,SECRET, orAPI_KEY(case-insensitive) — or starts withOPENBOX_,RETROACHIEVEMENTS_,EMUMOVIES_,GITHUB_,RA_,IGDB_, orGAMEYFIN_— is stripped, so plugins cannot read tokens, secrets, or host state out of the environment. - Install only packages you wrote or audited. The bundled catalog is documentation-oriented; installing from it still runs downloaded code.
Lifecycle
- Install (
/api/plugins/install): a directory or ZIP package is staged, validated, and moved intoplugins/<id>/. Updates replace the previous version atomically; a failed install or update restores the previous version. - Enable/disable (
/api/plugins/toggle): disabled plugin ids persist inplugins-state.json; disabled plugins are skipped by every hook. - Run: on each hook event, enabled plugins that declare the hook execute in sorted (alphabetical) directory order, each as a separate process.
- Remove (
/api/plugins/remove): the package moves toplugins/.removed/<id>-<timestamp>(recoverable) and its disabled state, trust grant, and permission grants are cleared, so a reinstall comes back enabled and untrusted. - Trust (
GET/POST /api/v2/plugins/trust, 1.14.0+): on a host where the bubblewrap sandbox cannot be created, a plugin does nothing until you grant per-plugin trust in the Plugins manager. The grant is bound to the installed package's SHA-256, so any update that changes the package invalidates it and re-prompts. There is deliberately no global "trust everything" switch;OPENBOX_ALLOW_UNSANDBOXED_PLUGINS=1remains the operator-level override. - Permissions (
POST /api/v2/plugins/permissions, 1.14.0+): declared permissions are denied by default. The only permission in 1.14.0 isnetwork; granting it adds--share-netto the plugin's sandbox argv, and without the grant the plugin keeps the no-network sandbox. A grant can never exceed the manifest declaration. - Settings (
GET/POST /api/v2/plugins/settings, 1.14.0+): a manifestsettingsJSON Schema subset renders a form in the Plugins manager; validated values are stored per plugin and delivered to the hook aspayload["settings"].
Hook execution points
| Hook | When | Effect on result |
|---|---|---|
library | Every /api/library read (cached for 30 seconds), skipped in safe mode | May rewrite the games list; the response uses the last plugin's output when it is a dict with a games list of the same length |
before_launch | At launch, after profile/archive resolution, skipped in safe mode | May rewrite args/cwd or cancel with {"cancel": true, "error": "..."}; structurally invalid output is discarded with a warning and the original launch command is used |
after_session | After a session ends (history recorded, plugins run unless safe mode) | Ignored (return value discarded) |
command | When a palette command is invoked (1.14.0+) | Returns an optional notification to show; unlike the chained hooks, its errors surface to the caller |
library_source | On every library read, for manifests declaring it (1.14.0+) | Contributes imported games, namespaced plugin:<plugin_id>:<id> and merged into the public library |
events | On each lifecycle event: app_startup, app_shutdown, scan_finished, playtime_milestone, game_added, game_removed, game_updated (1.14.0+) | Result discarded; a failure is logged and never breaks the emitting operation |
Safe mode
OPENBOX_SAFE_MODE=1 (any non-empty value) in the process environment disables plugin execution process-wide: library hooks, before_launch, after_session, the command, library_source, and events hooks, and the webhook dispatcher all skip. The setting is exposed to the UI as settings.safe_mode. It is the first thing to try when a plugin causes launch or library failures.
Limits and failure behavior
- One JSON object per invocation over stdin/stdout.
- Input payload and output capped at 2 MiB each; oversized output is ignored with a warning.
- 5-second timeout per plugin; a timeout, crash, or invalid JSON logs a warning and the plugin's result is skipped (the previous result passes through). Palette
commandruns are the exception: a hook error there is surfaced to the API caller as a400. - A nonzero exit is logged with the last 400 bytes of stderr.
- The plugin environment is cleaned:
PYTHONPATH,PYTHONHOME,LD_PRELOAD,LD_LIBRARY_PATHremoved,PYTHONNOUSERSITE=1.
Plugin v2 routes
GET /api/v2/plugins/commands— manifest-declared commands from enabled, valid plugins;POST /api/v2/plugins/commandwith{plugin_id, command}runs thecommandhook.GET /api/v2/plugins/trust?id=<id>/POST /api/v2/plugins/trustwith{id, trusted}— per-plugin sandbox-bypass trust (checksum-bound, updates re-prompt).POST /api/v2/plugins/permissionswith{id, permissions}— grant declared permissions (currentlynetworkonly).GET /api/v2/plugins/settings?id=<id>/POST /api/v2/plugins/settingswith{id, values}— schema, stored values, and validated save.GET /api/v2/plugins/catalog— catalog entries enriched withinstalled,installed_version,update_available;sandboxis a top-level field of the response.