openclaw - ✅(Solved) Fix [Bug]: plugins install --profile X writes to default extensions dir instead of profile's state dir [1 pull requests, 1 comments, 2 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#69960Fetched 2026-04-23 07:30:57
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Timeline (top)
commented ×1cross-referenced ×1labeled ×1

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

Root Cause

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

Fix Action

Fixed

PR fix notes

PR #69971: fix(plugins): honor profile state dir on install + follow symlinks in discovery (closes #69960)

Description (problem / solution / changelog)

Summary

  • Problem: openclaw --profile <name> plugins install <spec> writes to ~/.openclaw/extensions/ instead of the profile's state dir. The profile's gateway then can't see the installed plugin and fails silently. Separately, discoverInDirectory() silently skips plugins installed as symlinks.
  • Why it matters: Profile isolation is the mechanism by which multiple openclaw deployments coexist on one machine; when install breaks it, the entire profile workflow is broken. Symlink discovery breaks the common npm link-style local dev workflow for extension authors.
  • What changed: runPluginInstallCommand now resolves extensionsDir at the CLI layer (after applyCliProfileEnv) and forwards it into every direct installPluginFromNpmSpec / installPluginFromPath call. discoverInDirectory() now stats symlink targets and honors directory symlinks, with a diagnostics warn for broken ones.
  • What did NOT change: resolveStateDir unchanged. installPluginFromClawHub / installPluginFromMarketplace / installBundledPluginSource unchanged (different install paths, no extensionsDir parameter on their current signatures). Same-shape bug in src/commands/channel-setup/plugin-install.ts:188 is flagged below as a follow-up rather than fixed here.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra
  • Plugins (install + discovery)

Linked Issue/PR

  • Closes #69960
  • Related: same-shape latent bug in src/commands/channel-setup/plugin-install.ts:188 (follow-up candidate, not fixed here)
  • This PR fixes a bug or regression

Root Cause

  • Root cause: src/plugins/install.ts imports CONFIG_DIR from ../utils.js. CONFIG_DIR is a top-level constant evaluated at module load, before the CLI's applyCliProfileEnv mutates process.env.OPENCLAW_STATE_DIR. The install-time fallback extensionsDir = params.extensionsDir ? resolveUserPath(params.extensionsDir) : path.join(CONFIG_DIR, "extensions") therefore always resolves against the non-profile default when runPluginInstallCommand omits extensionsDir. resolveStateDir(process.env, os.homedir) called at the CLI layer (after applyCliProfileEnv) does the right thing — this PR just moves the resolution to that point.
  • Missing detection / guardrail: no unit test asserted that the CLI forwards a profile-aware extensionsDir into the installer. plugins uninstall and plugins list already followed this pattern; plugins install silently diverged.
  • Contributing context: symlink arm of discoverInDirectory had never been tested; entry.isDirectory() is documented to return false for symlinks in Node's fs.Dirent API.

Regression Test Plan

  • Coverage level that should have caught this: unit test at the CLI boundary that mocks installPluginFromNpmSpec and asserts extensionsDir is forwarded (now added as src/cli/plugins-install-command.extensions-dir.test.ts). Symlink discovery: unit test against a tmpdir containing a symlink target + a broken symlink (now added in src/plugins/discovery.test.ts).
  • Tests added (6):
    • forwards a profile-aware extensionsDir when OPENCLAW_STATE_DIR is set
    • forwards an extensionsDir derived from resolveStateDir when no override is set
    • follows a symlinked plugin directory to its target package
    • warns and skips a broken symlink without throwing
    • still discovers plain subdirectory plugin packages (no regression)
    • still skips regular non-extension files (no regression)

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No (install path still fetches the same npm tarball; only the write destination changes)
  • Command/tool execution surface changed? No
  • Data access scope changed? Slightly narrower — profile installs now write into the profile's own state dir instead of leaking into the non-profile default. This tightens isolation, not loosens it.
  • Symlink discovery: we only follow one level via a single fs.statSync, not a recursive walk; broken/unresolvable symlinks fall through to the existing diagnostics.warn channel. No arbitrary FS traversal introduced.

Repro + Verification

Environment

  • OS: macOS 26.5 (arm64)
  • Runtime/container: Node v25.9.0, OpenClaw main
  • Relevant config: OPENCLAW_STATE_DIR=~/.openclaw-ledger + --profile ledger

Steps

  1. OPENCLAW_STATE_DIR=~/.openclaw-ledger openclaw --profile ledger plugins install @ctatedev/mcp-bridge
  2. ls ~/.openclaw-ledger/extensions
  3. openclaw --profile ledger plugins list

Expected

  • Plugin installed under ~/.openclaw-ledger/extensions/@ctatedev/mcp-bridge and visible to the profile's plugins list.

Actual (before fix)

  • Plugin installed under ~/.openclaw/extensions/...; profile's plugins list empty.

Actual (after fix)

  • Plugin installed under ~/.openclaw-ledger/extensions/...; profile's plugins list shows it.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

Before (reporter's repro from #69960):

$ OPENCLAW_STATE_DIR=~/.openclaw-ledger openclaw --profile ledger plugins install @ctatedev/mcp-bridge
✓ installed into ~/.openclaw/extensions/@ctatedev/mcp-bridge    # wrong!
$ ls ~/.openclaw-ledger/extensions
# empty

After:

$ OPENCLAW_STATE_DIR=~/.openclaw-ledger openclaw --profile ledger plugins install @ctatedev/mcp-bridge
✓ installed into ~/.openclaw-ledger/extensions/@ctatedev/mcp-bridge
$ ls ~/.openclaw-ledger/extensions
@ctatedev/

Test run:

vitest.cli.config.ts      → 963/963 passed (incl. 2 new)
vitest.plugins.config.ts  → 1106/1106 passed (incl. 4 new)
oxlint on touched files   → 0 warnings, 0 errors

Human Verification

  • Verified scenarios: ran the reporter's exact repro locally against a scratch profile state dir, confirmed the installed extension ends up under <profile>/extensions/ and is visible to the profile's gateway plugin discovery. Confirmed plugins list --profile X shows the installed extension.
  • Edge cases checked:
    • Install with no OPENCLAW_STATE_DIR override → still writes to default ~/.openclaw/extensions/ (no regression).
    • --link dry-run and real install both forwarded the new extensionsDir.
    • Symlink pointing at a directory: discovered.
    • Broken symlink: logged as diagnostics.warn, not thrown.
    • Regular file entry: still skipped.
  • What I did NOT verify: the installPluginFromClawHub / installPluginFromMarketplace / installBundledPluginSource code paths — they have separate signatures and I intentionally left them alone. Haven't reproduced the src/commands/channel-setup/plugin-install.ts:188 same-shape bug that's flagged as a follow-up.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • Existing users on the default (no-profile) path see identical behavior. Profile users go from "silently broken" to "works as documented." No user action required.

Risks and Mitigations

  • Risk: regressing the default (no-profile) install path.
    • Mitigation: regression test forwards an extensionsDir derived from resolveStateDir when no override is set asserts the default resolution still lands at the pre-fix target.
  • Risk: following a symlink that loops or points outside the intended plugin tree.
    • Mitigation: we only follow one level of symlink via a single fs.statSync, not a recursive walk — the rest of the discovery pipeline operates on the resolved target the same way it operates on a regular directory. Broken/unresolvable symlinks fall through to the existing diagnostics.warn channel.

Thanks @FrancisLyman for the precise root-cause trace and the exact fix site.

Changed files

  • src/cli/plugins-install-command.extensions-dir.test.ts (added, +220/-0)
  • src/cli/plugins-install-command.ts (modified, +12/-0)
  • src/plugins/discovery.test.ts (modified, +64/-0)
  • src/plugins/discovery.ts (modified, +20/-1)
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

Steps to reproduce

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

Expected behavior

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

Actual behavior

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

OpenClaw version

2026.4.20, Node 22 LTS

Operating system

Ubuntu 24.04

Install method

No response

Model

opus 4.7

Provider / routing chain

local

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

extent analysis

TL;DR

The issue can be fixed by ensuring the ledger profile's gateway scans the correct directory for installed plugins, which is ~/.openclaw/extensions/, or by modifying the discoverInDirectory function to handle symlinks properly.

Guidance

  • Verify that the OPENCLAW_STATE_DIR environment variable is set correctly to ~/.openclaw-ledger and that the discoverInDirectory function is scanning the correct directory.
  • Consider modifying the discoverInDirectory function to use fs.statSync to follow symlinks or emit a warning when a symlink entry is skipped to improve debugging.
  • Check the installation path of plugins using openclaw --profile ledger plugins install <path> to ensure it matches the expected directory.
  • Review the code to ensure that the discoverInDirectory function is correctly handling directory scanning and symlink detection.

Example

const fs = require('fs');

// Example of using fs.statSync to follow symlinks
function discoverInDirectory(dir) {
  const entries = fs.readdirSync(dir);
  entries.forEach((entry) => {
    const stats = fs.statSync(`${dir}/${entry}`);
    if (stats.isDirectory()) {
      // Handle directory
    } else if (stats.isSymbolicLink()) {
      // Handle symlink
    }
  });
}

Notes

The provided information suggests a regression issue with the openclaw command and its interaction with the ledger profile's gateway. The discoverInDirectory function's handling of symlinks may be a contributing factor. However, without more detailed logs or code snippets, it's challenging to provide a definitive solution.

Recommendation

Apply a workaround by modifying the discoverInDirectory function to handle symlinks properly, as this will likely resolve the silent failure issue and improve debugging capabilities.

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

Version: v2026.4.20, Node 22 LTS, Ubuntu 24.04 Repro: openclaw --profile ledger plugins install <path> installs to /.openclaw/extensions/ but the ledger profile's gateway (with OPENCLAW_STATE_DIR=/.openclaw-ledger) scans ~/.openclaw-ledger/extensions/ Secondary bug: discoverInDirectory uses entry.isDirectory() which skips symlinks silently — recommend either using fs.statSync to follow symlinks, or emit a warning when a symlink entry is skipped Impact: silent failure, no log output, hard to diagnose (cost us ~1hr)

Still need to ship something?

×6

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

Back to top recommendations

TRENDING