openclaw - ✅(Solved) Fix feat(cli): `skills uninstall` design proposal + prior art reconciliation [1 pull requests, 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#63272Fetched 2026-04-09 07:56:01
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
1
Participants
Timeline (top)
cross-referenced ×2mentioned ×2subscribed ×2

Add an openclaw skills uninstall <slug> CLI command that cleanly removes a ClawHub-installed skill from the active workspace. Mirrors the existing openclaw skills install flow and the plugins uninstall pattern in src/plugins/uninstall.ts.

Scope is deliberately narrow: workspace directory + .clawhub/lock.json entry. Sandbox copies and dashboard UI are out of scope (see "Non-goals").

Filing this as a design proposal before writing the PR, because:

  1. A user already asked for this in #14264 (open 8 weeks, no maintainer response)
  2. A previous attempt (#17503) targeted a different surface (dashboard + bundled skills) and was closed as stale with two unresolved critical issues flagged by greptile. I want to explicitly address one of them (path traversal) and stay out of the surface the other one touched (.gitignore / bundled dir resolution).

Happy to drop this if maintainers prefer a different shape — I'd rather align up front than get auto-closed.

Error Message

  • planSkillUninstall: nothing installed → both flags false, no error
  1. Should uninstall warn or refuse when the skill is not a ClawHub install (no .clawhub/origin.json)? My current proposal is to proceed anyway, because manual skill management is a legitimate case.

Root Cause

Filing this as a design proposal before writing the PR, because:

  1. A user already asked for this in #14264 (open 8 weeks, no maintainer response)
  2. A previous attempt (#17503) targeted a different surface (dashboard + bundled skills) and was closed as stale with two unresolved critical issues flagged by greptile. I want to explicitly address one of them (path traversal) and stay out of the surface the other one touched (.gitignore / bundled dir resolution).

Fix Action

Fixed

PR fix notes

PR #17503: feat(dashboard): add Uninstall button for bundled skills

Description (problem / solution / changelog)

Summary

  • Adds a skills.uninstall gateway RPC method that removes a bundled skill directory and appends it to .gitignore (preventing it from returning on git pull/update)
  • Adds an "Uninstall" button (danger style) in the Skills dashboard view for bundled skills
  • Follows the same patterns as the existing skills.install flow

Changes

Backend

  • src/gateway/protocol/schema/agents-models-skills.ts — New SkillsUninstallParamsSchema
  • src/gateway/protocol/schema/types.ts — New SkillsUninstallParams type
  • src/gateway/protocol/index.ts — Register validator + exports
  • src/gateway/server-methods/skills.tsskills.uninstall handler (validates params, resolves bundled dir, deletes skill folder, appends to .gitignore)
  • src/gateway/server-methods.ts — Add skills.uninstall to admin-scoped methods

Frontend

  • ui/src/ui/controllers/skills.tsuninstallSkill() controller function
  • ui/src/ui/views/skills.ts — "Uninstall" button for openclaw-bundled skills
  • ui/src/ui/app-render.ts — Wire onUninstall handler

Motivation

Currently, removing a bundled skill requires manually deleting its folder and editing .gitignore. This PR automates the process with a single click from the dashboard.

Test plan

  • Open dashboard → Skills → Built-in Skills
  • Click "Uninstall" on a bundled skill
  • Verify skill folder is deleted
  • Verify skills/<name>/ is appended to .gitignore
  • Verify skill disappears from list after refresh
  • Verify non-bundled skills do not show the button
  • Verify disabled skills do not show the button

🤖 Generated with Claude Code

<!-- greptile_comment --> <h3>Greptile Summary</h3>

Adds a skills.uninstall RPC endpoint and corresponding dashboard UI to allow one-click removal of bundled skills. The backend handler resolves the bundled skills directory, deletes the skill folder, and attempts to append the path to .gitignore to prevent the skill from returning on git pull. The frontend adds a "danger"-styled Uninstall button for openclaw-bundled skills, following the same controller/view patterns as the existing skills.install flow.

  • Two critical issues were previously flagged and remain unresolved: path traversal vulnerability (no validation that the resolved path stays within the bundled directory) and .gitignore update silently never executes (resolveOpenClawPackageRootSync({}) is called with empty options, causing it to always return null)
  • Protocol schema, type exports, validator registration, and admin authorization are all wired correctly following existing conventions
  • Frontend controller and view changes cleanly mirror the existing install flow
<h3>Confidence Score: 1/5</h3>
  • This PR has two unresolved critical issues — a path traversal vulnerability that enables arbitrary directory deletion, and a .gitignore update that silently never executes.
  • Score of 1 reflects two previously-flagged but unresolved critical issues: (1) The name parameter is only validated as a non-empty string, allowing values like ../../etc to escape the bundled directory and trigger recursive deletion of arbitrary directories via fs.promises.rm. (2) resolveOpenClawPackageRootSync({}) is called with an empty options object, which means all candidate resolution paths are empty and it always returns null, silently skipping the .gitignore update — one of the two stated goals of the feature. The frontend and protocol changes are clean and follow existing patterns.
  • src/gateway/server-methods/skills.ts needs path traversal protection on the resolved skill directory (line 173) and correct options passed to resolveOpenClawPackageRootSync (line 185).

<sub>Last reviewed commit: b141a6f</sub>

<!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Changed files

  • src/gateway/protocol/index.ts (modified, +7/-0)
  • src/gateway/protocol/schema/agents-models-skills.ts (modified, +7/-0)
  • src/gateway/protocol/schema/types.ts (modified, +2/-0)
  • src/gateway/server-methods.ts (modified, +1/-0)
  • src/gateway/server-methods/skills.ts (modified, +67/-0)
  • ui/src/ui/app-render.ts (modified, +2/-0)
  • ui/src/ui/controllers/skills.ts (modified, +27/-0)
  • ui/src/ui/views/skills.ts (modified, +12/-0)

Code Example

openclaw skills uninstall <slug>
  [--dry-run]     show what would be removed, no changes
  [--yes]         skip confirmation prompt
  [--json]        machine-readable output

---

$ openclaw skills uninstall my-skill
The following will be removed:
  - workspace directory:  /Users/alice/.openclaw/workspace/skills/my-skill
  - lockfile entry:       .clawhub/lock.json#my-skill

Note: sandbox copies are refreshed automatically on next agent start.

Proceed with uninstall? [y/N] y
Uninstalled my-skill:
  ✓ removed workspace directory
  ✓ removed lockfile entry

---

$ openclaw skills uninstall my-skill --dry-run
The following would be removed:
  - workspace directory:  /Users/alice/.openclaw/workspace/skills/my-skill
  - lockfile entry:       .clawhub/lock.json#my-skill
(dry-run: no changes will be made)

---

$ openclaw skills uninstall nonexistent-skill
Skill 'nonexistent-skill' is not installed in /Users/alice/.openclaw/workspace

---

export type SkillUninstallPlan = {
  slug: string
  workspaceDir: string
  skillDir: string | null        // null if resolution fails (should not happen after slug validation)
  skillDirExists: boolean
  lockfileEntryExists: boolean
  isClawHubInstall: boolean      // based on presence of .clawhub/origin.json
}

export type SkillUninstallResult = {
  plan: SkillUninstallPlan
  removedSkillDir: boolean
  removedLockfileEntry: boolean
  warnings: string[]
}

export async function planSkillUninstall(
  workspaceDir: string,
  slug: string,
): Promise<SkillUninstallPlan>

export async function executeSkillUninstall(
  plan: SkillUninstallPlan,
  logger: { info: (msg: string) => void },
): Promise<SkillUninstallResult>

---

async function removeSkillFromLockfile(
  workspaceDir: string,
  slug: string,
): Promise<boolean>
RAW_BUFFERClick to expand / collapse

Summary

Add an openclaw skills uninstall <slug> CLI command that cleanly removes a ClawHub-installed skill from the active workspace. Mirrors the existing openclaw skills install flow and the plugins uninstall pattern in src/plugins/uninstall.ts.

Scope is deliberately narrow: workspace directory + .clawhub/lock.json entry. Sandbox copies and dashboard UI are out of scope (see "Non-goals").

Filing this as a design proposal before writing the PR, because:

  1. A user already asked for this in #14264 (open 8 weeks, no maintainer response)
  2. A previous attempt (#17503) targeted a different surface (dashboard + bundled skills) and was closed as stale with two unresolved critical issues flagged by greptile. I want to explicitly address one of them (path traversal) and stay out of the surface the other one touched (.gitignore / bundled dir resolution).

Happy to drop this if maintainers prefer a different shape — I'd rather align up front than get auto-closed.

Motivation

Today there is no way to uninstall a ClawHub-installed skill via CLI. Users must rm -rf ~/.openclaw/workspace/skills/<slug>/, which has a concrete bug that manual deletion cannot fix:

The .clawhub/lock.json entry is left behind. Next time the user runs openclaw skills update --all, the lockfile still lists the skill, and updateSkillsFromClawHub() will pull it back down. The skill reappears and the user cannot figure out why.

This is not a cosmetic "symmetry with install" request — it's a real correctness bug that only an uninstall command can fix, because the fix needs to touch both the filesystem and the workspace lockfile atomically.

Non-goals (explicit)

These are intentionally excluded to keep the PR reviewable and the blast radius small:

  • Sandbox copies (~/.openclaw/sandboxes/*/skills/<slug>/). syncSkillsToWorkspace() at src/agents/skills/workspace.ts:801 already does fsp.rm(targetSkillsDir, { recursive: true, force: true }) before every sync, so the next agent start will reconcile automatically. Actively deleting sandbox copies from the uninstall command would race with the serializeByKey('syncSkills:${targetDir}') lock. A separate --purge-sandboxes flag can be added in a follow-up PR if users ask for it.
  • Bundled skills and the dashboard UI (the surface of #17503). This PR only touches the CLI and only operates on workspace ClawHub skills.
  • openclaw.json mutation. ClawHub install does not write to skills.entries / plugins.*, so there is nothing to clean up. If a future install change starts writing there, the uninstall path should be updated at the same time to preserve the inverse relationship.
  • --all / multi-slug / wildcard. Single slug only for the first PR. Batch operations can land as a separate PR once this one is reviewed.
  • Trash / undo. rm is final. Matches the existing plugins uninstall behavior.

Proposed design

CLI surface

openclaw skills uninstall <slug>
  [--dry-run]     show what would be removed, no changes
  [--yes]         skip confirmation prompt
  [--json]        machine-readable output

Registered in src/cli/skills-cli.ts right after install (currently line 92), using the same Commander.js pattern as the existing subcommands.

Confirmation uses promptYesNo() from src/cli/prompt.ts:5, matching the existing project-wide convention. --yes honors the global yes-flag. --dry-run matches the flag name already used by plugins CLI (src/cli/plugins-cli.uninstall.test.ts:47).

Example output

$ openclaw skills uninstall my-skill
The following will be removed:
  - workspace directory:  /Users/alice/.openclaw/workspace/skills/my-skill
  - lockfile entry:       .clawhub/lock.json#my-skill

Note: sandbox copies are refreshed automatically on next agent start.

Proceed with uninstall? [y/N] y
Uninstalled my-skill:
  ✓ removed workspace directory
  ✓ removed lockfile entry
$ openclaw skills uninstall my-skill --dry-run
The following would be removed:
  - workspace directory:  /Users/alice/.openclaw/workspace/skills/my-skill
  - lockfile entry:       .clawhub/lock.json#my-skill
(dry-run: no changes will be made)
$ openclaw skills uninstall nonexistent-skill
Skill 'nonexistent-skill' is not installed in /Users/alice/.openclaw/workspace

Implementation shape

Two new functions in src/agents/skills-clawhub.ts:

export type SkillUninstallPlan = {
  slug: string
  workspaceDir: string
  skillDir: string | null        // null if resolution fails (should not happen after slug validation)
  skillDirExists: boolean
  lockfileEntryExists: boolean
  isClawHubInstall: boolean      // based on presence of .clawhub/origin.json
}

export type SkillUninstallResult = {
  plan: SkillUninstallPlan
  removedSkillDir: boolean
  removedLockfileEntry: boolean
  warnings: string[]
}

export async function planSkillUninstall(
  workspaceDir: string,
  slug: string,
): Promise<SkillUninstallPlan>

export async function executeSkillUninstall(
  plan: SkillUninstallPlan,
  logger: { info: (msg: string) => void },
): Promise<SkillUninstallResult>

And one internal helper:

async function removeSkillFromLockfile(
  workspaceDir: string,
  slug: string,
): Promise<boolean>

Both planSkillUninstall and the path resolution inside executeSkillUninstall go through resolveSkillInstallDir() at src/agents/skills-clawhub.ts:121, which already uses resolveSafeInstallDir() to reject slugs like ../foo, foo/bar, or names containing path separators.

This is a deliberate choice to avoid the path-traversal vulnerability that was flagged on #17503: resolveSafeInstallDir() guarantees the resolved directory stays inside {workspaceDir}/skills/, so skills uninstall ../../etc/passwd cannot trigger a fs.rm on anything outside the skills root.

Lockfile mutation reuses readClawHubSkillsLockfile() and writeClawHubSkillsLockfile() at src/agents/skills-clawhub.ts:143-175 — same helpers that install and update already use, so the write-path is already exercised and tested.

Test plan

Three test files to update, matching the existing layout:

src/agents/skills-clawhub.test.ts — unit:

  • planSkillUninstall: skill present + lockfile entry present → both flags true
  • planSkillUninstall: skill dir present but no lockfile entry (manually copied) → lockfileEntryExists: false
  • planSkillUninstall: nothing installed → both flags false, no error
  • planSkillUninstall: unsafe slugs (.., ../foo, foo/bar, empty, non-ASCII) → throws via resolveSafeInstallDir()
  • executeSkillUninstall: happy path removes directory and lockfile entry
  • executeSkillUninstall: directory removal fails (simulate EPERM via mock) → warning collected, no throw, lockfile still cleaned
  • executeSkillUninstall: only lockfile entry exists, no directory → cleans lockfile, reports removedSkillDir: false
  • removeSkillFromLockfile: removes only the target slug, preserves other entries

src/cli/skills-cli.commands.test.ts — command integration (mock loadConfig, resolveAgentWorkspaceDir, defaultRuntime, promptYesNo):

  • uninstall <slug> without --yes calls promptYesNo; cancellation is a no-op
  • uninstall <slug> --yes skips prompt and executes
  • uninstall <slug> --dry-run prints plan, does not call executeSkillUninstall
  • uninstall <slug> --json emits valid JSON to stdout
  • uninstall <not-installed> logs "not installed" and exits 0
  • planSkillUninstall throws (unsafe slug) → defaultRuntime.exit(1) called

src/cli/skills-cli.test.ts — format:

  • formatUninstallPlan(plan, { dryRun: true }) snapshot
  • formatUninstallPlan(plan, { dryRun: false }) snapshot
  • formatUninstallResult(result) with and without warnings

Test fixtures reuse writeSkill() from src/agents/skills.e2e-test-helpers.ts.

Estimated diff size

Around +500 / -0, split roughly:

  • src/agents/skills-clawhub.ts — +120 (two functions + one helper + types)
  • src/cli/skills-cli.ts — +70 (one .command block)
  • src/cli/skills-cli.format.ts — +40
  • Tests — +250

Similar shape to the existing plugins uninstall implementation.

Clarifications / corrections for anyone designing this

While researching this I noticed a few misconceptions that show up in related discussions (including my own earlier draft of this proposal). Leaving these here so the next person doesn't waste cycles on them:

  1. _meta.json does not exist in this codebase. The per-skill install marker is .clawhub/origin.json (written by writeClawHubSkillOrigin() at src/agents/skills-clawhub.ts:201), and the workspace-level manifest is .clawhub/lock.json (ClawHubSkillsLockfile type at src/agents/skills-clawhub.ts:29-38). These are only written for ClawHub-installed skills; manually-dropped skills have neither.

  2. Skills and plugins are separate subsystems. Skills use skills.entries, skills.load, skills.limits, skills.install in openclaw.json. Plugins use plugins.entries, plugins.installs, plugins.allow. ClawHub skill install does not touch plugins.*, so there's nothing to clean up in the plugins config on uninstall. Likewise, skills do not write to ~/.openclaw/extensions/<slug>/ — that directory is exclusively for plugin installs.

  3. Sandbox directory names are not agent-main-<hash>. The pattern is {slugified-session-key}-{sha256[:8]}, generated by slugifySessionKey() at src/agents/sandbox/shared.ts:7. The prefix is whatever the session key slugs down to, not a fixed string.

  4. A skill's extensions/ subdirectory is self-contained. It travels inside the skill's workspace dir and is deleted along with the parent dir. There is no separate ~/.openclaw/extensions/<slug>/ mirror to worry about.

Relation to prior work

  • #14264 — original feature request from @Donmeusi. This proposal implements the feature they asked for, scoped to CLI.
  • #17503 — previous attempt (@diegofornalha), closed as stale after greptile flagged a path traversal vulnerability and a silent .gitignore write bug. That PR targeted the dashboard / bundled skills / gateway RPC surface, which is orthogonal to this proposal (CLI / workspace ClawHub skills). The path traversal concern is addressed here by routing all slug → path resolution through the existing resolveSafeInstallDir() helper.

Open questions for maintainers

  1. Is the CLI + workspace-ClawHub scope the right starting point, or would you prefer a gateway RPC first (so the dashboard can eventually reuse it)?
  2. Should uninstall warn or refuse when the skill is not a ClawHub install (no .clawhub/origin.json)? My current proposal is to proceed anyway, because manual skill management is a legitimate case.
  3. Is there a maintainer interested in this area? If not, I'll hold off on the PR — I don't want to go through the same stale-close cycle as #17503.

Happy to iterate on any of the above before writing code.

extent analysis

TL;DR

Implement the openclaw skills uninstall <slug> CLI command to cleanly remove a ClawHub-installed skill from the active workspace by deleting the workspace directory and updating the .clawhub/lock.json entry.

Guidance

  • Implement the planSkillUninstall and executeSkillUninstall functions in src/agents/skills-clawhub.ts to handle the uninstallation logic.
  • Use the existing resolveSafeInstallDir helper to prevent path traversal vulnerabilities.
  • Update the src/cli/skills-cli.ts file to register the new uninstall command and handle user input and confirmation.
  • Write comprehensive tests for the new functionality, including unit tests, command integration tests, and format tests.

Example

export async function planSkillUninstall(
  workspaceDir: string,
  slug: string,
): Promise<SkillUninstallPlan> {
  // implementation details
}

export async function executeSkillUninstall(
  plan: SkillUninstallPlan,
  logger: { info: (msg: string) => void },
): Promise<SkillUninstallResult> {
  // implementation details
}

Notes

  • The implementation should follow the proposed design and test plan outlined in the issue body.
  • The uninstall command should only operate on workspace ClawHub skills and not touch sandbox copies or the dashboard UI.
  • The implementation should avoid introducing any new security vulnerabilities, such as path traversal attacks.

Recommendation

Apply the proposed design and implementation to add the openclaw skills uninstall <slug> CLI command, as it addresses a real correctness bug and provides a clean way to remove ClawHub-installed skills from the active workspace.

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