`$.config.list()` / `$.config.set` have no plugin `userConfig` rows until `/reload-plugins` in desktop-hosted and headless sessions

Status Open
Reported on v2.1.295
Maintainer reply None cached
Activity 0 comments · opened Oct 9, 2026

Environment

  • Claude Code 2.1.295 (macOS, arm64)
  • Claude desktop app 2.26454.2 (Code tab); also reproduces with claude -p

Summary

According to the plugin API docs, each enabled plugin's non-sensitive userConfig fields appear as /config rows keyed <plugin>.<field>, and a plugin can change them with $.config.set. In sessions hosted by the desktop app, and in headless claude -p sessions, those rows are missing until something refreshes plugins. The plugin's hooks load and run normally, but:

  • $.config.list() returns only the engine's own rows. Plugin rows are missing, including the built-in cc-plugin-agents-md.instructionFiles.
  • $.config.set({ key: '<plugin>.<field>', value }) throws no /config row with key <plugin>.<field> ($.config.list names them).

After /reload-plugins in the same session, the rows appear and $.config.set works.

The rows appear to come from getAppState().plugins.enabled. That list is filled in by the interactive initial plugin load and by applyRefresh, and is apparently left empty in SDK-hosted and headless sessions until a refresh. The hook modules load through a separate path, so a plugin is running while its own rows don't exist.

Reproduction

  1. Install any plugin with a userConfig field, for example a field compactLabels on a plugin named statusline.
  2. Create a diagnostic plugin folder config-rows:

hooks/register.ts:
``ts
import type { Register } from 'claude-code'
const pluginRows = async ($) => (await $.config.list()).map((row) => row.key).filter((key) => key.includes('.')).join(', ')
export const register: Register = (on) => {
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'configrows', description: 'List plugin /config rows' })
return next(e)
})
on('command.run', { command: 'configrows' }, async ($) => ({ text: 'ROWS: ' + await pluginRows($) }))
}
``

  1. Run claude -p --plugin-dir ./config-rows "/configrows". The output is ROWS: (empty).
  2. Send /reload-plugins and then /configrows in one session:

``sh
printf '%s\n' \
'{"type":"user","message":{"role":"user","content":"/reload-plugins"}}' \
'{"type":"user","message":{"role":"user","content":"/configrows"}}' \
| claude -p --plugin-dir ./config-rows --input-format stream-json --output-format stream-json --verbose
`
The output now lists
statusline.colorMode, …, statusline.compactLabels, cc-plugin-agents-md.instructionFiles`.

In a desktop app Code-tab session, the same thing happens without -p: /configrows (or a plugin command that calls $.config.set) finds no plugin rows until /reload-plugins runs or something else refreshes plugins, such as enabling mod hot-reloading.

Expected

Plugin userConfig rows are present in $.config.list() and accepted by $.config.set whenever the plugin's hooks are loaded, on every surface, without a manual /reload-plugins.

Impact

Any plugin command that changes its own options through $.config.set fails in desktop sessions. Plugins have no API to trigger a refresh themselves, so the only workaround is to tell people to run /reload-plugins or edit ~/.claude/settings.json by hand.

View original on GitHub ↗