openclaw - 💡(How to fix) Fix Plugin discovery loads all dist/extensions/ manifests at boot regardless of tools.allow (~500 MB structural heap) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#70533Fetched 2026-04-24 05:56:49
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

Plugin discovery reads every manifest under dist/extensions/ via readFileSync and runs each through zod validation at boot — even when tools.allow restricts the enabled plugin set to a small subset. For an install with 104 bundled extensions and a 6-plugin allowlist, this contributes ~500 MB of resident heap that scales with bundle size, not user config.

Ask: apply the tools.allow filter at discovery time (before reading/validating the manifest), not only at enable time.

Root Cause

  • Ito (WhatsApp assistant running on a single Mac mini) carries a ~1 GB structural floor it can never drop below, even at idle, entirely for plugins it will never load.
  • A --max-old-space-size cap below ~1.5 GB OOMs at boot because ~1 GB is discovery overhead.
  • Related: #48644 (startup time + 2.2 GB peak) — same root cause from a different angle. Fixing discovery-time filtering would address both memory and most of the startup time.
  • Related: #69250 (register() thrash per boot) — also plugin-lifecycle-adjacent.

Fix Action

Workaround

None. The 104 extensions ship inside the openclaw npm package — users can't selectively remove them without breaking the install. Only upstream can filter at discovery.

Code Example

const names = await fs.readdir(extensionsDir);
const allowed = config.tools?.allow
  ? names.filter(n => config.tools.allow.includes(n))
  : names;
const manifests = await Promise.all(allowed.map(loadAndValidateManifest));
RAW_BUFFERClick to expand / collapse

Summary

Plugin discovery reads every manifest under dist/extensions/ via readFileSync and runs each through zod validation at boot — even when tools.allow restricts the enabled plugin set to a small subset. For an install with 104 bundled extensions and a 6-plugin allowlist, this contributes ~500 MB of resident heap that scales with bundle size, not user config.

Ask: apply the tools.allow filter at discovery time (before reading/validating the manifest), not only at enable time.

Environment

  • OpenClaw version: 2026.4.15 (also reproducible on 2026.3.24)
  • Node.js: v22 (macOS, /usr/local/opt/node)
  • OS: macOS 15.4 (Darwin 25.4.0)
  • Install method: npm i -g openclaw
  • Bundled extensions: 104 directories under dist/extensions/
  • Active plugins in config: 6 (tools.allow restricts to icecream-ito + a handful of providers)

Evidence

Captured a V8 sampling heap profile (node --heap-prof --heap-prof-interval=524288) against a warm, live gateway with workload. Aggregated top contributors by source file:

MB%LocationWhat it is
12721.3%node_modules/openclaw/[zod]Schema validation — every plugin's tool/config schemas compiled
10918.4%node:fs readFileSyncPlugin manifest + skills files read at boot
8914.9%node_modules/openclaw/[source-map]Source-map parsing (separate issue — also exacerbated by N manifests)
366.0%node:vm ScriptModule compilation
223.8%node_modules/openclaw/[jiti]Runtime TS transpile of manifests
203.3%node_modules/openclaw/[json5]Config parsing
172.8%node_modules/openclaw coreRuntime
101.7%icecream-ito-pluginUser's actual plugin

Combined zod + readFileSync + jiti + json5 + source-map + vm = ~75% of sampled heap (~450 MB) — all module-load overhead proportional to the number of bundled extensions, not the allowlist.

The user's plugin itself is 1.7%. The workload-side state (sessions, conversation history, search indices) is <5% combined and reclaimable by GC. The 500 MB floor is structural and persists across idle.

Current behavior

Discovery walks dist/extensions/*, reads each manifest with readFileSync, transpiles via jiti where needed, and validates with zod — before the tools.allow gate is consulted. With 104 bundled extensions, this happens 104 times at every boot.

The allowlist only prevents instantiation/registration later, not the discovery-time work that built the validated schema graph. Zod schemas, once compiled, are retained for the process lifetime.

Expected behavior

When tools.allow (or equivalent) is non-empty:

  1. Enumerate extension directory names only (cheap readdir).
  2. Filter against the allowlist.
  3. Only then: readFileSync + jiti + zod validate the manifests that survive the filter.

For the 6-of-104 case, this would save ~94% of the discovery cost (~470 MB).

Impact

  • Ito (WhatsApp assistant running on a single Mac mini) carries a ~1 GB structural floor it can never drop below, even at idle, entirely for plugins it will never load.
  • A --max-old-space-size cap below ~1.5 GB OOMs at boot because ~1 GB is discovery overhead.
  • Related: #48644 (startup time + 2.2 GB peak) — same root cause from a different angle. Fixing discovery-time filtering would address both memory and most of the startup time.
  • Related: #69250 (register() thrash per boot) — also plugin-lifecycle-adjacent.

Reproduction

  1. Install openclaw globally with all bundled extensions present (104).
  2. Configure tools.allow with a small subset (e.g., 6 plugins).
  3. Boot the gateway and wait for settle (~5 min).
  4. ps -o rss= on the gateway PID → resident ~1.2–1.3 GB.
  5. Run node --heap-prof --heap-prof-interval=524288 against the same config and aggregate by file-level location — readFileSync, zod, jiti, json5 dominate.

Heap profile (1.2 MB, loadable in Chrome DevTools → Memory → Load profile) and the aggregator script are available on request — not attaching inline due to GitHub issue size limits.

Workaround

None. The 104 extensions ship inside the openclaw npm package — users can't selectively remove them without breaking the install. Only upstream can filter at discovery.

Suggested fix sketch

In the plugin discovery path, approximately:

const names = await fs.readdir(extensionsDir);
const allowed = config.tools?.allow
  ? names.filter(n => config.tools.allow.includes(n))
  : names;
const manifests = await Promise.all(allowed.map(loadAndValidateManifest));

The loadAndValidateManifest step is where readFileSync + zod compile happens today. Gating that behind the allowlist check is the intended change.

Happy to test a fix against Ito's live config and report before/after RSS numbers.

extent analysis

TL;DR

Apply the tools.allow filter at plugin discovery time to reduce memory usage by only loading and validating allowed plugins.

Guidance

  • Modify the plugin discovery path to filter extension directory names against the tools.allow list before loading and validating manifests.
  • Use fs.readdir to enumerate extension directory names, then filter against the allowlist using names.filter(n => config.tools.allow.includes(n)).
  • Only load and validate manifests for allowed plugins using Promise.all(allowed.map(loadAndValidateManifest)).
  • Verify the fix by checking the resident memory usage of the gateway process after applying the filter.

Example

const names = await fs.readdir(extensionsDir);
const allowed = config.tools?.allow
  ? names.filter(n => config.tools.allow.includes(n))
  : names;
const manifests = await Promise.all(allowed.map(loadAndValidateManifest));

Notes

The suggested fix assumes that the loadAndValidateManifest function is responsible for loading and validating plugin manifests using readFileSync and zod. The fix may need to be adapted to fit the actual implementation details of the plugin discovery path.

Recommendation

Apply the workaround by modifying the plugin discovery path to filter extension directory names against the tools.allow list before loading and validating manifests, as this is expected to significantly reduce memory usage.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

When tools.allow (or equivalent) is non-empty:

  1. Enumerate extension directory names only (cheap readdir).
  2. Filter against the allowlist.
  3. Only then: readFileSync + jiti + zod validate the manifests that survive the filter.

For the 6-of-104 case, this would save ~94% of the discovery cost (~470 MB).

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING