Skip to content

Skill registry: changes to skills.load.extraDirs do not propagate to existing per-session skillsSnapshot #83782

@nathansmithopenclaw-alt

Description

Summary

Adding a new root to skills.load.extraDirs in ~/.openclaw/openclaw.json
makes the skill visible in openclaw skills list --agent <agent> but it
never reaches the per-session --plugin-dir mount that gets passed to
claude. Existing sessions keep using their cached skillsSnapshot and
never pick the new extraDirs roots up. Restarting Gateway / opening a fresh
session is required as a workaround.

Version

openclaw@2026.5.7 (npm-managed install).

Reproduce

  1. With an open OpenClaw agent session, add a new root to
    skills.load.extraDirs, e.g.
    "skills": { "load": { "extraDirs": ["/some/shared/skills"] } }
  2. Place a valid SKILL.md under /some/shared/skills/example-skill/.
  3. Confirm registry sees it: openclaw skills list --agent <agent> --json
    shows the skill with source: "openclaw-extra".
  4. Inspect the live plugin-dir handed to claude:
    ls /tmp/openclaw/openclaw-claude-skills-*/openclaw-skills/skills/
    The new skill is NOT present, even though plugin-generated extras with
    the same source: "openclaw-extra" label (e.g. browser-automation)
    ARE present.

Root cause

dist/refresh-*.js :: ensureSkillsWatcher(...) rebuilds its chokidar
watcher when watchTargets change (set of paths derived from extraDirs +
plugin skill dirs), but it does NOT call bumpSkillsSnapshotVersion(...)
in that branch. The per-session reuse gate at
dist/session-updates-*.js :: shouldRefreshSnapshotForVersion(...) only
compares snapshot versions, so existing sessions keep their stale
skillsSnapshot. Subsequent prepareClaudeCliSkillsPlugin(...) only
materializes skills present in that stale snapshot.

(File suffixes elided since they are per-release content hashes.)

Proposed fix

When ensureSkillsWatcher detects that pathsKey (the joined watch
targets) has changed vs the previous watcher, bump the snapshot version
once, with reason: "watch-targets". Minimal diff (logical, against
dist/refresh-*.js :: ensureSkillsWatcher):

   const watchTargets = resolveWatchTargets(workspaceDir, params.config);
   const pathsKey = watchTargets.join("|");
+  const watchTargetsChanged = Boolean(existing) && existing.pathsKey !== pathsKey;
   if (existing && existing.pathsKey === pathsKey && existing.debounceMs === debounceMs) return;
   ...
   watchers.set(workspaceDir, state);
+  if (watchTargetsChanged) {
+    bumpSkillsSnapshotVersion({
+      workspaceDir,
+      reason: "watch-targets",
+      changedPath: pathsKey,
+    });
+  }
 }

This is the narrowest fix: it doesn't broaden which sources may mount, it
only invalidates stale per-session snapshots when the set of watched skill
roots changes. SKILL.md file changes already bump via the existing watcher
events; this adds the missing equivalent for root-set changes from config
or plugin-skill path changes.

Workaround

Manually trigger a fresh session after editing extraDirs, or restart
Gateway, so the new sessions rebuild skillsSnapshot from scratch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    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