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
[email protected] (npm-managed install).
Reproduce
- With an open OpenClaw agent session, add a new root to
skills.load.extraDirs, e.g.
"skills": { "load": { "extraDirs": ["/some/shared/skills"] } }
- Place a valid
SKILL.md under /some/shared/skills/example-skill/.
- Confirm registry sees it:
openclaw skills list --agent <agent> --json
shows the skill with source: "openclaw-extra".
- 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.
Summary
Adding a new root to
skills.load.extraDirsin~/.openclaw/openclaw.jsonmakes the skill visible in
openclaw skills list --agent <agent>but itnever reaches the per-session
--plugin-dirmount that gets passed toclaude. Existing sessions keep using their cachedskillsSnapshotandnever pick the new extraDirs roots up. Restarting Gateway / opening a fresh
session is required as a workaround.
Version
[email protected](npm-managed install).Reproduce
skills.load.extraDirs, e.g."skills": { "load": { "extraDirs": ["/some/shared/skills"] } }SKILL.mdunder/some/shared/skills/example-skill/.openclaw skills list --agent <agent> --jsonshows the skill with
source: "openclaw-extra".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 chokidarwatcher when
watchTargetschange (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(...)onlycompares snapshot versions, so existing sessions keep their stale
skillsSnapshot. SubsequentprepareClaudeCliSkillsPlugin(...)onlymaterializes skills present in that stale snapshot.
(File suffixes elided since they are per-release content hashes.)
Proposed fix
When
ensureSkillsWatcherdetects thatpathsKey(the joined watchtargets) has changed vs the previous watcher, bump the snapshot version
once, with
reason: "watch-targets". Minimal diff (logical, againstdist/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 restartGateway, so the new sessions rebuild
skillsSnapshotfrom scratch.