Skip to content

[Bug]: plugins enable <nonexistent-id> writes a stale plugin config entry and exits 0 #73551

@bittoby

Description

@bittoby

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

openclaw plugins enable does not validate that is a discovered plugin before mutating config. A nonexistent plugin id is written to plugins.entries. with enabled: true, then the CLI prints a success message and exits 0.

Steps to reproduce

  1. Use OpenClaw 2026.4.26 from a source checkout.
  2. Run pnpm openclaw plugins enable totally-fake-plugin-xyz.
  3. Observe a warning that the plugin was not found.
  4. Observe the CLI still prints Enabled plugin "totally-fake-plugin-xyz". Restart the gateway to apply.
  5. Inspect ~/.openclaw/openclaw.json.
  6. Observe plugins.entries.totally-fake-plugin-xyz = { "enabled": true }.

Expected behavior

plugins enable should validate the id against the discovered plugin registry before writing config.

For a nonexistent id, it should:

  • Exit non-zero.
  • Print a clear error like Plugin not found: . Run `openclaw plugins list\ to see installed plugins.`
  • Not modify openclaw.json.

plugins disable should follow the same validation rule and should not create or mutate stale entries for nonexistent plugins.

Actual behavior

The CLI emits a not-found warning, writes the stale config entry, prints a success message, and exits 0.

Observed result:

Config warnings:
- plugins.entries.totally-fake-plugin-xyz: plugin not found: totally-fake-plugin-xyz (stale config entry ignored; remove it from plugins config)

Enabled plugin "totally-fake-plugin-xyz". Restart the gateway to apply.

Persisted config contains:

{
  "plugins": {
    "entries": {
      "totally-fake-plugin-xyz": {
        "enabled": true
      }
    }
  }
}

OpenClaw version

2026.4.26

Operating system

Ubuntu 24.04.4 LTS

Install method

pnpm dev

Model

N/A

Provider / routing chain

N/A

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Logs, screenshots, and evidence

=== Step 3: enable a nonexistent plugin id (the bug) ===

$ pnpm openclaw plugins enable totally-fake-plugin-xyz
> node scripts/run-node.mjs plugins enable totally-fake-plugin-xyz

- plugins.entries.totally-fake-plugin-xyz: plugin not found: totally-fake-plugin-xyz (stale config entry ignored; remove it from plugins config)
Config overwrite: /home/orin/.openclaw/openclaw.json (sha256 99d6be97c9b54512f3e4d898cf18173e807c2435d8ef8d45762a5c3818f95080 -> a5e6a17f68de9a8a02a07268947cd70400217ea4907fdf6f6d44eca3217394ac, backup=/home/orin/.openclaw/openclaw.json.bak)
Config warnings:
- plugins.entries.totally-fake-plugin-xyz: plugin not found: totally-fake-plugin-xyz (stale config entry ignored; remove it from plugins config)
Enabled plugin "totally-fake-plugin-xyz". Restart the gateway to apply.

$ echo $?
0

=== Persisted in config ===

$ python3 -c "
import json
d = json.load(open('/home/orin/.openclaw/openclaw.json'))
print('totally-fake-plugin-xyz =', d['plugins']['entries'].get('totally-fake-plugin-xyz'))
print('total entries:', len(d['plugins']['entries']))
"
totally-fake-plugin-xyz = {'enabled': True}
total entries: 5

=== Subsequent plugin command inherits the warning ===

$ pnpm openclaw plugins disable whatsapp
- plugins.entries.totally-fake-plugin-xyz: plugin not found: totally-fake-plugin-xyz (stale config entry ignored; remove it from plugins config)
... (then proceeds to disable whatsapp; the junk warning rides every subsequent plugins-touching command)

=== Counter-example: plugins inspect on the same id correctly errors out, no config mutation ===

$ pnpm openclaw plugins inspect totally-fake-plugin
Plugin not found: totally-fake-plugin
 ELIFECYCLE  Command failed with exit code 1.

$ python3 -c "
import json
d = json.load(open('/home/orin/.openclaw/openclaw.json'))
print('totally-fake-plugin in entries?', 'totally-fake-plugin' in d['plugins']['entries'])
"
totally-fake-plugin in entries? False

=== Recovery via stock CLI is blocked ===

$ pnpm openclaw plugins disable totally-fake-plugin-xyz
... (prints same warning, flips enabled to false, keeps the entry, exit 0)

$ python3 -c "
import json
d = json.load(open('/home/orin/.openclaw/openclaw.json'))
print(d['plugins']['entries'].get('totally-fake-plugin-xyz'))
"
{'enabled': False}

$ pnpm openclaw plugins uninstall totally-fake-plugin-xyz
Plugin: totally-fake-plugin-xyz
Will remove: config entry
Uninstall plugin "totally-fake-plugin-xyz"? [y/N] Warning: Detected unsettled top-level await at file:///home/orin/Gittensor/Test/openclaw/openclaw.mjs:233
  if (await tryImport("./dist/entry.js")) {
      ^

 ELIFECYCLE  Command failed with exit code 13.

(uninstall is interactive; in a non-TTY shell it crashes Node with exit 13 instead of failing cleanly with a "requires interactive TTY" error like other interactive subcommands)

=== Cleanup actually requires manual JSON editing ===

$ python3 -c "
import json
p = '/home/orin/.openclaw/openclaw.json'
d = json.load(open(p))
d['plugins']['entries'].pop('totally-fake-plugin-xyz', None)
json.dump(d, open(p,'w'), indent=2)
print('cleaned')
"
cleaned

Impact and severity

Affected users/systems/channels:
- Every operator who runs `openclaw plugins enable <id>` with a typo'd or otherwise nonexistent id. Linux directly observed (Ubuntu 24.04 / Node 22.22.2 / pnpm 10.33.0); the bug is in the CLI/config write path and platform-agnostic, so behavior is expected on macOS/Windows but only Linux was directly reproduced.
- Every plugin-touching command thereafter surfaces an inherited "stale config entry ignored; remove it from plugins config" warning until the operator manually edits openclaw.json.

Severity:
- Annoying with persistent state corruption. The config is silently mutated to include a non-functional entry; subsequent commands carry forward a warning the operator cannot dismiss without manual JSON editing.
- Not a security/data-loss bug per se — the entry is non-functional at load time (the gateway logs say "stale config entry ignored") — but it visibly degrades CLI output and trains operators to ignore warnings, which raises the cost of catching real plugins-config issues.
- Self-contradicting CLI output ("plugin not found" warning + "Enabled plugin <id>" success message in the same response) is a trust-eroding paper cut.

Frequency:
- Always, deterministic. 100% reproduction with any string that does not match an installed plugin id. Independent of model/provider/transport.

Consequence:
- Silent config corruption: ~/.openclaw/openclaw.json grows a junk entry on every fat-fingered enable.
- No clean recovery path through stock CLI: `plugins disable` keeps the entry; `plugins uninstall` requires an interactive TTY and crashes Node with exit 13 in non-TTY shells; the operator is forced to hand-edit the JSON.
- Misleading "success" exit code (0) breaks shell scripts and CI flows that branch on `openclaw plugins enable <id>` returning non-zero on a typo. Operators who automate plugin enablement against a list (e.g. provisioning scripts) will accept silent acceptance of stale ids.
- No grounded evidence of missed messages, failed onboarding, or extra cost.

Additional information

- Regression status: not classified as a Regression. Last-known-good not directly observed; no bisect performed.

- Likely fix locus: the action handler for `plugins enable` (commander definition under `src/cli/cli-plugins/` or wherever the parser lives in this build). The validator that emits the "plugin not found ... stale config entry ignored" warning already runs — promote it to a hard gate when the operator just supplied the id on the command line. The same gate should be added to `plugins disable` for symmetry, since today it also silently flips state on nonexistent ids.

- Suggested regression test: a unit test next to the `plugins enable` action that passes a definitely-not-registered id (e.g. seeded mock plugin registry containing only "real-plugin"; invoke enable with "fake-plugin"), and asserts (a) exit code is non-zero, (b) error message names the missing plugin and points at `plugins list`, (c) the config-write seam is not invoked. Pair with a parity test on `plugins disable` and (if the uninstall hang is fixed in the same PR) `plugins uninstall`.

- Related findings from the same session worth potentially separate issues:
  - `plugins uninstall <id>` requires an interactive TTY (prompts `[y/N]`) but does not detect non-TTY stdin and instead crashes Node with `Detected unsettled top-level await ... exit code 13`. Other interactive subcommands (e.g. `models auth login`) emit `requires interactive TTY` and exit 1 cleanly. Could file separately.
  - `plugins disable <nonexistent-id>` reports "Disabled plugin <id>. Restart the gateway to apply." with the same self-contradicting warning and keeps the junk entry. Same root cause as this issue (no validate-before-write), so a single PR can fix both surfaces.
  - `config unset <absent-path>` returns "Config path not found: <path>" with exit 1, and is non-idempotent — repeated unsets of the same already-absent path produce identical errors. Convention-dependent (npm idempotent, git non-idempotent), so this one is more debatable. Not bundling.

- Dedupe checked against the openclaw issue corpus on 2026-04-28: no existing open or closed issue matches the "plugins enable persists nonexistent plugin id" behavior. Closest text match (#65319, open) is about contradictory `plugins.allow` guidance for the `/lossless` command — different code path (allow-list discovery, not enable-write), not a duplicate.

- Not exercised in this repro: per-agent plugin overrides (`plugins.entries` is global config, but per-agent state files exist under `~/.openclaw/agents/<id>/agent/`); whether `plugins enable` against a plugin in a third-party marketplace surfaces the same gap; concurrent enable invocations across multiple shells (the config-write path uses `Config overwrite: ... backup=...` semantics so a race is possible but not directly observed).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionBehavior that previously worked and now fails

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions