openclaw - ✅(Solved) Fix [Bug]: Symbolic links for skill directories under ~/.openclaw/skills/ are not consistently resolved [2 pull requests, 2 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#49408Fetched 2026-04-08 00:55:30
View on GitHub
Comments
2
Participants
2
Timeline
7
Reactions
3
Timeline (top)
cross-referenced ×3commented ×2referenced ×2

Fix Action

Fixed

PR fix notes

PR #60144: fix(security): align audit symlink_escape boundary with skill loader

Description (problem / solution / changelog)

Bug

The skills.workspace.symlink_escape audit probe checks whether skill file realpaths escape the workspace root (~/.openclaw/workspace/), but the skill loader (resolveContainedSkillPath in src/agents/skills/workspace.ts) checks against the skills directory root (~/.openclaw/workspace/skills/).

This means symlinks like:

workspace/skills/my-skill -> ../ehoy/.claude/skills/my-skill

...resolve to workspace/ehoy/.claude/skills/my-skill, which is:

  • ✅ INSIDE the workspace root → audit says OK
  • ❌ OUTSIDE workspace/skills/ → loader silently rejects it

openclaw security audit reports no findings, but the skill is silently dropped at load time with zero user feedback.

How We Found It

We had 10 symlinked skills that were silently invisible for nearly a month after v2026.3.7 shipped the realpath enforcement in the loader. The daily-review cron skill stopped working but the model was finding and executing the skill through manual filesystem search ~80% of the time, masking the fact that formal skill discovery never included it. openclaw security audit showed nothing wrong.

Fix

Changed the audit probe to check against the skills directory realpath (workspace/skills/) instead of the workspace root, matching the loader's boundary. Updated finding messages to reflect the tighter boundary.

Test

Added a test case for the specific gap: a symlink that resolves inside the workspace root but outside the skills directory now correctly triggers the skills.workspace.symlink_escape finding.

All existing tests pass. The existing "warns when workspace skill files resolve outside workspace root" test continues to pass (those escapes are a superset of the new check).

AI-Assisted

This PR was authored by an AI agent (Claude Opus via OpenClaw). The code changes were verified against the existing test suite and a new test case was added. The agent reviewed git blame and commit history to confirm this was an oversight (the probe was added Mar 2 before the Mar 8 loader hardening), not intentional behavior.

Relates to #49408

Changed files

  • .agents/maintainers.md (added, +1/-0)
  • .agents/skills/openclaw-ghsa-maintainer/SKILL.md (added, +87/-0)
  • .agents/skills/openclaw-parallels-smoke/SKILL.md (added, +97/-0)
  • .agents/skills/openclaw-pr-maintainer/SKILL.md (added, +75/-0)
  • .agents/skills/openclaw-release-maintainer/SKILL.md (added, +267/-0)
  • .agents/skills/openclaw-test-heap-leaks/SKILL.md (added, +75/-0)
  • .agents/skills/openclaw-test-heap-leaks/agents/openai.yaml (added, +4/-0)
  • .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs (added, +553/-0)
  • .agents/skills/parallels-discord-roundtrip/SKILL.md (added, +62/-0)
  • .agents/skills/security-triage/SKILL.md (added, +108/-0)
  • .codex (added, +0/-0)
  • .detect-secrets.cfg (added, +45/-0)
  • .dockerignore (added, +70/-0)
  • .env.example (added, +80/-0)
  • .gitattributes (added, +3/-0)
  • .github/CODEOWNERS (added, +54/-0)
  • .github/ISSUE_TEMPLATE/bug_report.yml (added, +148/-0)
  • .github/ISSUE_TEMPLATE/config.yml (added, +8/-0)
  • .github/ISSUE_TEMPLATE/feature_request.yml (added, +70/-0)
  • .github/actionlint.yaml (added, +23/-0)
  • .github/actions/detect-docs-changes/action.yml (added, +53/-0)
  • .github/actions/ensure-base-commit/action.yml (added, +61/-0)
  • .github/actions/setup-node-env/action.yml (added, +99/-0)
  • .github/actions/setup-pnpm-store-cache/action.yml (added, +76/-0)
  • .github/codeql/codeql-javascript-typescript.yml (added, +18/-0)
  • .github/dependabot.yml (added, +127/-0)
  • .github/instructions/copilot.instructions.md (added, +64/-0)
  • .github/labeler.yml (added, +340/-0)
  • .github/pull_request_template.md (added, +149/-0)
  • .github/workflows/auto-response.yml (added, +530/-0)
  • .github/workflows/ci.yml (added, +1052/-0)
  • .github/workflows/codeql.yml (added, +137/-0)
  • .github/workflows/docker-release.yml (added, +389/-0)
  • .github/workflows/install-smoke.yml (added, +205/-0)
  • .github/workflows/labeler.yml (added, +877/-0)
  • .github/workflows/macos-release.yml (added, +93/-0)
  • .github/workflows/openclaw-npm-release.yml (added, +424/-0)
  • .github/workflows/plugin-npm-release.yml (added, +217/-0)
  • .github/workflows/sandbox-common-smoke.yml (added, +64/-0)
  • .github/workflows/stale.yml (added, +217/-0)
  • .github/workflows/workflow-sanity.yml (added, +98/-0)
  • .gitignore (added, +143/-0)
  • .jscpd.json (added, +16/-0)
  • .mailmap (added, +13/-0)
  • .markdownlint-cli2.jsonc (added, +55/-0)
  • .npmignore (added, +3/-0)
  • .npmrc (added, +4/-0)
  • .oxfmtrc.jsonc (added, +26/-0)
  • .oxlintrc.json (added, +39/-0)
  • .pi/extensions/diff.ts (added, +117/-0)
  • .pi/extensions/files.ts (added, +134/-0)
  • .pi/extensions/prompt-url-widget.ts (added, +190/-0)
  • .pi/extensions/redraws.ts (added, +26/-0)
  • .pi/extensions/ui/paged-select.ts (added, +82/-0)
  • .pi/git/.gitignore (added, +2/-0)
  • .pi/prompts/cl.md (added, +58/-0)
  • .pi/prompts/is.md (added, +22/-0)
  • .pi/prompts/landpr.md (added, +73/-0)
  • .pi/prompts/reviewpr.md (added, +134/-0)
  • .pre-commit-config.yaml (added, +157/-0)
  • .prettierignore (added, +1/-0)
  • .secrets.baseline (added, +13017/-0)
  • .shellcheckrc (added, +25/-0)
  • .swiftformat (added, +51/-0)
  • .swiftlint.yml (added, +150/-0)
  • .vscode/extensions.json (added, +3/-0)
  • .vscode/settings.json (added, +22/-0)
  • AGENTS.md (added, +286/-0)
  • CHANGELOG.md (added, +5325/-0)
  • CLAUDE.md (added, +1/-0)
  • CONTRIBUTING.md (added, +211/-0)
  • Dockerfile (added, +264/-0)
  • Dockerfile.sandbox (added, +24/-0)
  • Dockerfile.sandbox-browser (added, +36/-0)
  • Dockerfile.sandbox-common (added, +48/-0)
  • LICENSE (added, +21/-0)
  • Makefile (added, +4/-0)
  • README.md (added, +606/-0)
  • SECURITY.md (added, +322/-0)
  • Swabble/.github/workflows/ci.yml (added, +54/-0)
  • Swabble/.gitignore (added, +33/-0)
  • Swabble/.swiftformat (added, +8/-0)
  • Swabble/.swiftlint.yml (added, +43/-0)
  • Swabble/CHANGELOG.md (added, +11/-0)
  • Swabble/LICENSE (added, +21/-0)
  • Swabble/Package.resolved (added, +69/-0)
  • Swabble/Package.swift (added, +55/-0)
  • Swabble/README.md (added, +111/-0)
  • Swabble/Sources/SwabbleCore/Config/Config.swift (added, +77/-0)
  • Swabble/Sources/SwabbleCore/Hooks/HookExecutor.swift (added, +75/-0)
  • Swabble/Sources/SwabbleCore/Speech/BufferConverter.swift (added, +50/-0)
  • Swabble/Sources/SwabbleCore/Speech/SpeechPipeline.swift (added, +114/-0)
  • Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift (added, +62/-0)
  • Swabble/Sources/SwabbleCore/Support/Logging.swift (added, +41/-0)
  • Swabble/Sources/SwabbleCore/Support/OutputFormat.swift (added, +45/-0)
  • Swabble/Sources/SwabbleCore/Support/TranscriptsStore.swift (added, +45/-0)
  • Swabble/Sources/SwabbleKit/WakeWordGate.swift (added, +191/-0)
  • Swabble/Sources/swabble/CLI/CLIRegistry.swift (added, +71/-0)
  • Swabble/Sources/swabble/Commands/DoctorCommand.swift (added, +37/-0)
  • Swabble/Sources/swabble/Commands/HealthCommand.swift (added, +16/-0)

PR #60513: fix(security): align audit symlink_escape boundary with skill loader

Description (problem / solution / changelog)

The skills.workspace.symlink_escape audit probe checked whether skill file realpaths escaped the workspace root (~/.openclaw/workspace/), but the skill loader (resolveContainedSkillPath) checks against the skills directory root (~/.openclaw/workspace/skills/).

This mismatch meant symlinks like:

workspace/skills/my-skill -> workspace/other-dir/skills/my-skill

...would resolve inside the workspace root (audit says OK) but outside the skills directory (loader silently rejects). The skill would fail to load with zero feedback from openclaw security audit.

Fix: check against the skills directory realpath in the audit probe, matching the loader's boundary. Add test for the in-workspace but outside-skills-dir case.

Relates to #49408


Replaces #60144 which had a stale orphan branch causing 11k+ file diff.

Changed files

  • pnpm-lock.yaml (modified, +2/-0)
  • src/security/audit-extra.async.ts (modified, +5/-4)
  • src/security/audit.test.ts (modified, +29/-1)
RAW_BUFFERClick to expand / collapse

[Description]

When adding an OpenClaw Skill by creating a symbolic link (e.g., ln -s) linking an external folder to the ~/.openclaw/skills/ directory, the Agent fails to recognize or load the skill during session initialization.

[Steps to Reproduce]

  1. Prepare a physical skill directory: Create a valid skill folder at an arbitrary location, e.g., /Users/ray/my-custom-skill/ containing a valid SKILL.md.
  2. Create a symbolic link: Run ln -s /Users/ray/my-custom-skill/ ~/.openclaw/skills/my-custom-skill.
  3. Start/Restart OpenClaw: Observe the available skills in the startup context or via internal checks.
  4. Observation: The skill my-custom-skill is absent from the available skills list, even though the symbolic link exists and is traversable via the terminal.

[Expected Behavior]

OpenClaw should recursively resolve symbolic links within the skills/ scan path, allowing developers to manage skills in central repositories while using them via links.

[Environment]

  • OpenClaw Version: v2026.3.13
  • OS: macOS 26.3 (Apple Silicon)
  • Node.js: v25.6.1

[Additional Context]

Converting the symbolic link into a regular physical directory (cp -r) resolves the issue immediately, confirming that the loading logic specifically struggles with symlinks.

extent analysis

Fix Plan

To resolve the issue with OpenClaw not recognizing skills installed via symbolic links, we need to modify the skill loading logic to recursively resolve symbolic links.

Step-by-Step Solution

  1. Update the skill loading function: Modify the function responsible for loading skills to use a recursive directory traversal that follows symbolic links.
  2. Use fs.realpathSync(): Utilize Node.js's fs module to resolve the real path of the symbolic link.
  3. Implement recursive directory traversal: Use a library like glob or implement a custom recursive function to traverse the directory tree, following symbolic links.

Example Code

const fs = require('fs');
const path = require('path');

// Function to load skills, including those installed via symbolic links
function loadSkills(skillsDir) {
  const skills = [];
  fs.readdirSync(skillsDir).forEach(file => {
    const filePath = path.join(skillsDir, file);
    const stat = fs.lstatSync(filePath);
    if (stat.isDirectory()) {
      // Recursively traverse directories
      skills.push(...loadSkills(filePath));
    } else if (stat.isSymbolicLink()) {
      // Resolve symbolic link and traverse the linked directory
      const realPath = fs.realpathSync(filePath);
      skills.push(...loadSkills(realPath));
    } else if (file === 'SKILL.md') {
      // Load the skill
      skills.push(filePath);
    }
  });
  return skills;
}

// Load skills from the ~/.openclaw/skills/ directory
const skillsDir = path.join(process.env.HOME, '.openclaw', 'skills');
const skills = loadSkills(skillsDir);
console.log(skills);

Verification

To verify that the fix worked, restart OpenClaw and check the available skills list. The skill installed via a symbolic link should now be recognized and loaded correctly.

Extra Tips

  • Ensure that the fs and path modules are properly imported and utilized in your code.
  • Be cautious when working with symbolic links to avoid infinite loops or unexpected behavior.
  • Consider using a library like glob to simplify recursive directory traversal.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Symbolic links for skill directories under ~/.openclaw/skills/ are not consistently resolved [2 pull requests, 2 comments, 2 participants]