What this covers

How the Plugins sidebar entry works and what you do when you need to add, remove, or temporarily disable a plugin. Plugin install is a Composer-side workflow (or a Docker build-arg workflow if you run the container stack); the editor is a read-only browser.

For the developer-side view of plugins (the API a plugin uses to contribute modules, events, and menu items), see the Concept: Plugin discovery and Build a Module tutorial.

Walkthrough

Step 1: See what is installed

Click Plugins in the sidebar. The page shows two sections:

  • A table of booted plugins with five columns:
    • Plugin: the plugin's name (core/editor for the built-in editor plugin, vendor/package for Composer- installed plugins).
    • Version: the installed version string. For Composer plugins this is the Composer manifest version (matches the git tag Composer resolved); for core plugins it is the string the plugin's version() method returns.
    • Events: the PSR-14 events the plugin subscribes to. Empty if the plugin does not listen to any.
    • Editor modules: the editor module slugs the plugin registers (each becomes a /editor/<slug>/ route).
    • Menu items: the sidebar menu items the plugin contributes.
  • A disabled section below the table, listing any plugin FQCN entries in plugins.disabled (see Step 3 below). Empty most of the time.

This view is read-only. There is no "install", "remove", "enable", or "disable" button.

Step 2: Install a plugin

How you install depends on whether you run Scriptor natively or in Docker. The persisting paths are:

Native install (host with PHP + Composer):

composer require bigins/scriptor-markdown-pages

Scriptor's composer.json already has a repositories block that points at the VCS sources for bigins/* plugins, so no extra setup is needed for those. Third-party plugins on Packagist work the same way.

After the require finishes, reload the editor. The new plugin appears in the table; any sidebar items the plugin contributes appear in the sidebar.

Docker (compose stack):

Container filesystems below the volumes are immutable across restarts, so the workflow is to bake the plugin into the image via a build arg in your compose override:

services:
  scriptor:
    build:
      args:
        SCRIPTOR_PLUGIN_REPOS: "https://github.com/bigin/scriptor-markdown-pages"
        SCRIPTOR_PLUGINS: "bigins/scriptor-markdown-pages:^0.1"

Then:

docker compose up -d --build

Scriptor's Dockerfile runs composer require $SCRIPTOR_PLUGINS during image build; the plugin lands in vendor/ and survives every restart, recreation, or deploy.

Multiple plugins go in the same args, space-separated. Each non-Packagist plugin needs its repo URL in SCRIPTOR_PLUGIN_REPOS too:

SCRIPTOR_PLUGIN_REPOS: "https://github.com/bigin/scriptor-markdown-pages https://github.com/bigin/scriptor-markdown-feed"
SCRIPTOR_PLUGINS: "bigins/scriptor-markdown-pages:^0.1 bigins/scriptor-markdown-feed:^0.1"

Trap: docker exec scriptor composer require ... works immediately because the discovery cache invalidates from installed.json mtime, but the change lives in the running container's writable layer and is wiped by the next docker compose down && up. Use it for "does this plugin boot" probes; never as an install path for anything you want to keep.

Step 3: Disable a plugin without uninstalling

Sometimes you want to temporarily turn a plugin off (debugging a suspected interaction, A/B comparing site behaviour) without running composer remove. Set $config['plugins']['disabled'] in data/settings/custom.scriptor-config.php:

return [
    'plugins' => [
        'disabled' => [
            'bigins\\ScriptorMarkdownPages\\Plugin',
        ],
    ],
];

The value is the plugin's fully-qualified class name (the same string the Plugins table's first column shows for that plugin, with the backslashes doubled because PHP string escaping). The plugin stays in vendor/; the PluginManager skips its register() hook on the next request.

Reload the editor. The plugin's row moves from the table into the Disabled section. Any sidebar items it contributed are gone.

To re-enable, remove the entry from the array (or comment out the whole disabled block) and reload.

Step 4: Uninstall a plugin

Reverse of the install path you used. First check whether the plugin owns database schema, because that changes the order of the steps.

Most plugins are stateless: they add events, modules, or menu items, but no database schema of their own. Those you remove with a plain composer remove. Some plugins are lifecycle plugins: on install they register their own iManager fields or categories, and they ship an uninstall hook to remove that schema again. If the plugin's README mentions a bin/scriptor plugin:install step, it is a lifecycle plugin and needs the extra CLI step below.

Native, stateless plugin:

composer remove bigins/scriptor-markdown-pages

The plugin disappears from vendor/composer/installed.json; the discovery cache invalidates on the next request; the row is gone from the Plugins table; any editor modules and menu items the plugin contributed are gone.

Native, lifecycle plugin (order matters):

Run the uninstall hook before removing the code, so the plugin can drop the schema it created while its class is still loadable:

bin/scriptor plugin:uninstall vendor/package-name   # removes its schema, clears state
composer remove vendor/package-name                 # then drop the code

Add --purge-data to the plugin:uninstall call if you also want the plugin's per-row values stripped from the database (the default keeps them, in case you reinstall later).

If you run composer remove first by mistake: the uninstall hook can no longer run, so the schema the plugin created stays in the database and the plugin shows up as an ORPHAN row in bin/scriptor plugin:list. Clear the leftover state entry with:

bin/scriptor plugin:cleanup-orphan vendor/package-name

This only clears the state entry. It cannot remove the orphaned schema (the plugin's code is gone, so Scriptor no longer knows what to remove). To clean the schema too, reinstall the package, run plugin:uninstall properly, then composer remove. That is the whole reason the uninstall order matters.

Docker:

Drop the plugin from the SCRIPTOR_PLUGINS build arg in your compose override (or remove the whole arg block if it was the only plugin), then rebuild:

docker compose up -d --build

The new image is built without that composer require step, so the plugin never lands in vendor/. For a lifecycle plugin, run docker compose exec scriptor bin/scriptor plugin:uninstall vendor/package-name before you rebuild without the arg, for the same reason as the native flow above; otherwise its schema is left orphaned.

Important: plugin-owned data is not removed. Content trees the plugin manages, settings it persists, uploads it took ownership of, or database rows it wrote stay where they are. Check the plugin's README for its on-disk footprint and delete those paths manually if you want a clean state. For bigins/scriptor-markdown-pages, that means the per-theme themes/<name>/content/ directories with _index.md files.

What to check after

After installing a plugin:

  • The plugin appears as a row in the Plugins table with the version Composer resolved.
  • If the plugin contributes events, modules, or menu items, those columns show their values; empty columns are also fine.
  • The frontend or editor surface the plugin claims to provide works (check the plugin's README for what to expect).

After disabling via plugins.disabled:

  • The plugin's row moves to the Disabled section.
  • Sidebar items the plugin contributed are gone.
  • The frontend or editor behaviour the plugin provided is gone.

After uninstalling:

  • The plugin no longer appears anywhere on the Plugins page.
  • The vendor/<vendor>/<package>/ directory is gone.
  • For a lifecycle plugin, bin/scriptor plugin:list shows no ORPHAN row for it. An ORPHAN row means the code was removed before its uninstall hook ran; see the troubleshooting entry below.

Troubleshooting

"No plugins booted" but I just installed one

Three causes, most to least common:

  1. The package is not marked as a Scriptor plugin. The plugin's composer.json needs "type": "scriptor-plugin" and "extra": {"scriptor": {"plugin": "<FQCN>"}} for the PluginManager to discover it. A package that does not declare both is a regular library, not a plugin.
  2. Discovery cache is stale. Touch vendor/composer/installed.json (Composer normally does this on install, but a manually-edited vendor tree can leave it inconsistent). The PluginManager invalidates its discovery cache by mtime.
  3. Plugin's register() threw. Check data/logs/ for a stack trace. A plugin that throws during boot is silently skipped and the editor continues without it.

The Plugins table shows the wrong version

The table prefers the Composer manifest version (the git tag Composer resolved) over the plugin's version() string. If the two disagree, the manifest version wins (and is shown). The plugin author probably forgot to bump the version() string in their release commit; harmless, but worth noting if you are debugging against a specific release.

"Plugin disabled" but its sidebar item is still there

The disabled list takes the plugin's fully-qualified class name, not the package name. bigins/scriptor-markdown-pages in the disabled list does nothing because the PluginManager looks for the class name (e.g. bigins\\ScriptorMarkdownPages\\Plugin). Check the Plugins table's first column for the exact string to disable.

plugin:list shows an ORPHAN row after uninstalling

This means a lifecycle plugin's code was removed (composer remove or a rebuild without the SCRIPTOR_PLUGINS arg) before its uninstall hook ran, so its state entry was left behind and the schema it created is still in the database. Clear the state entry with bin/scriptor plugin:cleanup-orphan vendor/package-name. To also remove the orphaned schema, reinstall the package, run bin/scriptor plugin:uninstall vendor/package-name, then composer remove. Always run plugin:uninstall before removing a lifecycle plugin's code to avoid this in the first place.

After docker compose up -d --build, the plugin still does not boot

Two checks:

  1. The build arg name is exactly SCRIPTOR_PLUGINS (all caps, underscore). A typo (SCRIPTOR_PLUGIN singular) silently has no effect.
  2. The --build flag is actually rebuilding the image. docker compose up -d without --build reuses the existing image even when the compose file changed. docker compose build --no-cache scriptor then up -d forces a clean rebuild if you suspect a stale image.

See also