[BUG] Desktop app ignores prependPlugins: installed plugins it sideloads at initialize load in alphabetical order

Status Fixed / completed
Reported on v2.1.289
Maintainer reply None cached
Activity 0 comments · opened Oct 7, 2026 · closed Oct 8, 2026

Summary

In the Claude desktop app (Code tab), prependPlugins in ~/.claude/settings.json has no effect on the order of installed plugins' hooks modules. The plugins load in id-sorted (alphabetical) order instead, so the AbovePrompt band and every other hook chain stack in a different order than in the CLI.

The same settings, same plugins and same engine binary honor prependPlugins in every CLI launch I tried (claude, claude -p, and stream-json mode).

Environment

  • Claude desktop app 2.26454.0 (bundles Claude Code 2.1.289)
  • Claude Code CLI 2.1.291 (same behavior as 2.1.289 for the CLI paths)
  • macOS
  • Five plugins installed at user scope from a local directory marketplace (claude plugin marketplace add <folder>), each a function-hooks module that draws a row in the AbovePrompt band
  • ~/.claude/settings.json lists all five in prependPlugins in the wanted order; no managed settings on the machine

Expected

The hooks modules load in the prependPlugins order, as the engine does in the CLI: engine.create's e.plugins reads a, b, c, d, e in the listed order, and the band stacks the same way.

Actual

In a desktop Code tab, e.plugins reads the five plugins alphabetically and the band stacks alphabetically. prependPlugins is ignored.

Cause (as far as I could trace it)

The desktop app does not leave installed plugins to the engine's own loader. At spawn it passes every installed plugin whose marketplace is listed in ~/.claude/plugins/known_marketplaces.json to the engine as an initialize.plugins entry of { type: "local", path: <installPath> } (the [CCD] Passing N plugin(s) to SDK path in the app; only plugins from an unknown marketplace are "left to the CLI's own loader"). The list is built in id-sorted order.

The engine then loads those as plugin-directory plugins, and prependPlugins seating applies only to plugins the engine's installed-plugin loader seats, so the sideloaded order stands.

Reproduction

A tiny probe plugin that records the fold order:

// hooks/register.ts
import type { Register } from 'claude-code'
let plugins: readonly string[] = []
export const register: Register = on => {
  on('engine.create', async ($, e, next) => { plugins = e.plugins; return next(e) })
  on('session.start', async ($, e, next) => {
    await $.fs.write('/tmp/plugin-order.txt', plugins.join('\n'))
    return next(e)
  })
}
  1. Install a few function-hook plugins from a directory marketplace and list them in prependPlugins in a non-alphabetical order.
  2. CLI: claude -p --plugin-dir ./probe "ok" → /tmp/plugin-order.txt shows the prependPlugins order. Same with an interactive claude, and with --output-format stream-json --input-format stream-json.
  3. Desktop: open a Code tab with the probe folder enabled for hot reloading → the file shows alphabetical order.
  4. Headless replay of what the desktop does flips the CLI result too:
printf '%s\n' '{"type":"control_request","request_id":"x","request":{"subtype":"initialize","plugins":[{"type":"local","path":"<cache>/plugin-b/<ver>"},{"type":"local","path":"<cache>/plugin-a/<ver>"}]}}' \
  '{"type":"user","message":{"role":"user","content":"ok"}}' \
| claude --input-format stream-json --output-format stream-json --await-initialize --plugin-dir ./probe

→ the order is plugin-b, plugin-a (the order given), regardless of prependPlugins. Giving the entries in the prependPlugins order restores it, so the engine keeps whatever order the app sends.

Suggested fix

Either have the desktop app order its sideloaded entries by prependPlugins (and appendPlugins) before sending them, or have the engine apply prependPlugins seating to sideloaded entries whose path matches an installed plugin's install path.

View original on GitHub ↗