Skip to content

feat(cli): add nemoclaw <sandbox> skill remove command#1962

Open
sinha-abhash wants to merge 6 commits intoNVIDIA:mainfrom
sinha-abhash:feat/skill-remove-command
Open

feat(cli): add nemoclaw <sandbox> skill remove command#1962
sinha-abhash wants to merge 6 commits intoNVIDIA:mainfrom
sinha-abhash:feat/skill-remove-command

Conversation

@sinha-abhash
Copy link
Copy Markdown

@sinha-abhash sinha-abhash commented Apr 16, 2026

Summary

Adds nemoclaw <sandbox> skill remove <name> to complement the existing skill install command (#1844 / #1845), providing a complete skill lifecycle from the CLI.

Motivation

After skill install was added, there was no way to cleanly remove a skill from a running sandbox. Users had to manually SSH in, delete two directories, and clear sessions.json — mirroring the same problem skill install was created to solve.

What this does

nemoclaw dtx-claw skill remove my-skill
  ✓ Skill 'my-skill' removed
  • Removes the immutable upload dir: /sandbox/.openclaw/skills/<name>/
  • Removes the OpenClaw mirror dir: $HOME/.openclaw/skills/<name>/
  • Clears sessions.json so the agent re-discovers remaining skills on the next session
  • Verifies the directory is gone after removal
  • Validates the skill name against [A-Za-z0-9._-] before any SSH operation — shell injection rejected client-side
  • Prints a clear error when the named skill is not installed

Usage

nemoclaw <sandbox> skill install <path>   # existing
nemoclaw <sandbox> skill remove <name>   # new

Run nemoclaw <sandbox> skill --help for full usage.

Edge cases tested

Scenario Result
Multi-file skill with subdirectory Entire directory tree removed cleanly
Wrong case name not installed error, exit 1
Missing name argument Usage printed, exit 1
Shell injection (skill;rm -rf /, ../../../etc/passwd, $(evil)) Rejected by validateSkillName before any SSH call
Remove skill while TUI is active Skill gone on next reconnect (same behaviour as install)
sessions.json cleared Confirmed {} after removal

Changes

src/lib/skill-install.ts

  • Added removeSkill() — removes upload dir, mirror dir, clears sessions
  • Added verifyRemove() — confirms directory is gone post-removal
  • Added validateSkillName() — rejects shell metacharacters in CLI name input

src/nemoclaw.ts

  • Extended sandboxSkillInstall() to dispatch install vs remove
  • Updated --help output and global help text
  • SSH config temp file always cleaned up in finally block

src/lib/skill-install.test.ts

  • Added validateSkillName tests — valid names, empty, spaces, metacharacters
  • Added removeSkill unit test — SSH-failure path returns success=false + warning
  • Added verifyRemove unit test — SSH-failure returns false (conservative/safe)

All 1611 existing tests continue to pass.

Summary by CodeRabbit

  • New Features

    • Added a sandbox "skill remove" command with staged removal, per-step messages/warnings, and post-removal verification; installer now dispatches install/remove subcommands.
  • Documentation

    • Help/usage updated to document the combined installer and skill remove flow and name requirements.
  • Bug Fixes

    • Strengthened name validation to reject "." and "..".
    • Existence checks now consider mirrored install locations and treat unreachable probes as non-fatal warnings.
  • Tests

    • New unit tests for name validation, removal/verification, and unreachable-SSH scenarios.

Adds `nemoclaw <sandbox> skill remove <name>` to complement the
existing `skill install` command, providing a complete skill
lifecycle from the CLI.

## What this does

- Removes the skill from the immutable upload dir:
  /sandbox/.openclaw/skills/<name>/
- Removes the OpenClaw mirror dir:
  $HOME/.openclaw/skills/<name>/
- Clears sessions.json so the agent re-discovers remaining skills
  on the next session (same mechanism as a fresh install)
- Verifies the directory is gone after removal
- Rejects invalid/unsafe skill names before any SSH operation
- Prints a clear error when the named skill is not installed

## Changes

src/lib/skill-install.ts
- Updated module-level comment to mention skill remove
- Added removeSkill() - removes upload dir, mirror dir, clears sessions
- Added verifyRemove() - confirms directory is gone post-removal
- Added validateSkillName() - rejects shell metacharacters in CLI input

src/nemoclaw.ts
- Extended sandboxSkillInstall() to dispatch install vs remove
- Updated --help output to document both subcommands
- Updated global help text to list skill remove
- Validates skill name with validateSkillName() before any SSH ops
- Checks skill exists before attempting removal (clear error if missing)
- SSH config temp file always cleaned up in finally block

src/lib/skill-install.test.ts
- Added describe(validateSkillName) - valid names, empty, spaces, metacharacters
- Added describe(removeSkill) - SSH-failure path returns success=false + warning
- Added describe(verifyRemove) - SSH-failure returns false (conservative)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Adds skill removal APIs and CLI handling; tightens skill-name validation to reject ./..; extends existence checks to include OpenClaw mirror location and to return null on SSH probe failure; adds unit tests covering name validation, conservative existence behavior, and removal/verification flows.

Changes

Cohort / File(s) Summary
Core library
src/lib/skill-install.ts
Added validateSkillName(name), changed checkExisting() to return `boolean
Tests
src/lib/skill-install.test.ts
Expanded tests: assert parseFrontmatter rejects ./.., comprehensive validateSkillName cases, new tests for removeSkill, verifyRemove, and checkExisting using an unreachable SSH config to assert conservative false/null outcomes and warning reporting.
CLI / Integration
src/nemoclaw.ts
Replaced previous installer dispatcher with subcommand routing; added sandboxSkillRemove() which validates names, ensures sandbox live, resolves session and paths, writes a temp SSH config (mode 0o600), calls checkExisting(), runs removeSkill() and streams messages (special-casing Warning:), verifies removal via verifyRemove(), and cleans up temp config; updated help text.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Nemoclaw as nemoclaw.ts
    participant Agent as agentRuntime
    participant SkillInstall as skill-install
    participant SSH as SSH/RemoteFS

    User->>Nemoclaw: sandbox skill remove <name>
    Nemoclaw->>Nemoclaw: validateSkillName(name)
    Nemoclaw->>Nemoclaw: ensureLiveSandboxOrExit()
    Nemoclaw->>Agent: getSessionAgent(sandbox)
    Agent-->>Nemoclaw: session + ctx
    Nemoclaw->>SkillInstall: resolveSkillPaths(name)
    SkillInstall-->>Nemoclaw: paths
    Nemoclaw->>Nemoclaw: write temp SSH config (mode 0o600)
    Nemoclaw->>SkillInstall: checkExisting(ctx, paths)
    SkillInstall->>SSH: stat uploadDir/SKILL.md and mirrorDir/SKILL.md (if OpenClaw)
    SSH-->>SkillInstall: exists / failure (-> null)
    SkillInstall-->>Nemoclaw: boolean|null
    Nemoclaw->>SkillInstall: removeSkill(ctx, paths)
    SkillInstall->>SSH: rm -rf uploadDir (and mirrorDir if applicable), clear sessions or emit hint
    SSH-->>SkillInstall: results + warnings
    SkillInstall-->>Nemoclaw: RemoveResult (messages, success)
    Nemoclaw->>SkillInstall: verifyRemove(ctx, paths)
    SkillInstall->>SSH: stat for absence of uploadDir and mirrorDir
    SSH-->>SkillInstall: absent|present
    Nemoclaw->>Nemoclaw: delete temp SSH config
    Nemoclaw-->>User: print messages and success/failure
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ericksoa
  • kjw3

Poem

🐰 I sniffed the names and chased the dots away,
I hopped through configs under midnight gray,
Mirrors checked and old sessions cleared,
Warnings flagged where stranded files sneered,
Now the burrow’s tidy — ready for play!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): add nemoclaw skill remove command' is concise, specific, and accurately reflects the main change—adding a new CLI command for skill removal that complements the existing skill install functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/skill-remove-command

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/nemoclaw.ts (1)

1340-1568: Split skill install and skill remove into dedicated handlers.

sandboxSkillInstall() now owns subcommand help, argument validation, two SSH-config lifecycles, and both install/remove workflows. That pushes this dispatcher well past the repo's complexity target and makes the next skill subcommand harder to change safely. Extract the remove flow and install flow into separate helpers, and leave this function to parse the subcommand and delegate.

As per coding guidelines, Limit cyclomatic complexity to 20 in JavaScript/TypeScript files, with target of 15.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 1340 - 1568, Current sandboxSkillInstall is
doing parsing plus both install and remove workflows; extract the remove and
install workflows into two small helpers (e.g.
handleSkillRemove(ctxNameOrSandbox, args) and
handleSkillInstall(ctxNameOrSandbox, args) or similar) and leave
sandboxSkillInstall to only parse subcommand, validate arguments, and delegate.
Move the SSH-config lifecycle (captureOpenshell(...), tmpSshConfig creation,
fs.writeFileSync, and fs.unlinkSync cleanup) into the helpers so each has its
own try/finally and uses the same ctx shape ({ configFile, sandboxName }); move
code that validates skillName (skillInstall.validateSkillName),
checkExisting/remove/verifyRemove into the remove helper (use
agentRuntime.getSessionAgent and skillInstall.resolveSkillPaths there) and move
frontmatter parsing, collectFiles, uploadDirectory, postInstall, and
verifyInstall into the install helper (again resolving agent and paths inside
it). Ensure sandboxSkillInstall still performs top-level argument/usage printing
and only calls the appropriate helper with sandboxName and the remaining args.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/skill-install.ts`:
- Around line 423-428: validateSkillName currently allows "." and ".." because
the regex matches them; update validateSkillName(name: string) to explicitly
reject a single "." and a double ".." (e.g., check name !== "." && name !==
"..") while keeping the existing /^[A-Za-z0-9._-]+$/ and length > 0 checks so
behavior remains the same for all other names; also ensure this change is
mirrored where the earlier frontmatter/skill-name validation is implemented so
both checks stay aligned.
- Around line 379-420: The removal currently reports success and verifyRemove()
checks only paths.uploadDir so if paths.mirrorDir (OpenClaw mirror) wasn't
removed the CLI can lie; update the removal return value and verification to
require the mirror deletion when paths.isOpenClaw is true: in the remover
function make success combine removedUploadDir && (if paths.isOpenClaw ?
removedMirrorDir : true) and in verifyRemove(ctx, paths) have sshExec test both
shellQuote(paths.uploadDir) and, when paths.isOpenClaw && paths.mirrorDir,
shellQuote(paths.mirrorDir) (both must be GONE); also update the existence check
logic used by checkExisting() to probe the mirror SKILL.md
(paths.mirrorDir/SKILL.md) in addition to uploadDir/SKILL.md so retries won't be
blocked.

---

Nitpick comments:
In `@src/nemoclaw.ts`:
- Around line 1340-1568: Current sandboxSkillInstall is doing parsing plus both
install and remove workflows; extract the remove and install workflows into two
small helpers (e.g. handleSkillRemove(ctxNameOrSandbox, args) and
handleSkillInstall(ctxNameOrSandbox, args) or similar) and leave
sandboxSkillInstall to only parse subcommand, validate arguments, and delegate.
Move the SSH-config lifecycle (captureOpenshell(...), tmpSshConfig creation,
fs.writeFileSync, and fs.unlinkSync cleanup) into the helpers so each has its
own try/finally and uses the same ctx shape ({ configFile, sandboxName }); move
code that validates skillName (skillInstall.validateSkillName),
checkExisting/remove/verifyRemove into the remove helper (use
agentRuntime.getSessionAgent and skillInstall.resolveSkillPaths there) and move
frontmatter parsing, collectFiles, uploadDirectory, postInstall, and
verifyInstall into the install helper (again resolving agent and paths inside
it). Ensure sandboxSkillInstall still performs top-level argument/usage printing
and only calls the appropriate helper with sandboxName and the remaining args.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f679918-19f5-4eff-b916-7665b1b7d101

📥 Commits

Reviewing files that changed from the base of the PR and between d90096d and 68beb05.

📒 Files selected for processing (3)
  • src/lib/skill-install.test.ts
  • src/lib/skill-install.ts
  • src/nemoclaw.ts

Comment thread src/lib/skill-install.ts
Comment thread src/lib/skill-install.ts Outdated
@wscurran wscurran added NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). enhancement: feature Use this label to identify requests for new capabilities in NemoClaw. labels Apr 16, 2026
@wscurran
Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR that proposes an enhancement to add a skill remove command, which could help improve the user experience and provide a complete skill lifecycle from the CLI.


Possibly related open PRs:


Possibly related open issues:

… parseFrontmatter

Both functions accepted single-dot and double-dot names because the
regex /^[A-Za-z0-9._-]+$/ allows '.'. In the remove path this would
expand to rm -rf '/sandbox/.openclaw/skills/..' which deletes the
parent .openclaw or skills/ directory instead of a single skill.

Fix: add explicit name !== '.' && name !== '..' guards in both
validateSkillName (CLI input) and parseFrontmatter (SKILL.md name
field), keeping the two checks aligned as CodeRabbit flagged.

Add test cases for '.' and '..' in both validateSkillName and
parseFrontmatter describe blocks. All 33 skill-install tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/lib/skill-install.ts (1)

405-420: ⚠️ Potential issue | 🟠 Major

Require mirror deletion for OpenClaw before reporting removal success.

On Line [406], success ignores mirror deletion; and Lines [417-420] verify only uploadDir. In OpenClaw mode, this can report “removed” while $HOME/.openclaw/skills/<name> still exists.

Proposed fix
 export function removeSkill(
   ctx: SshContext,
   paths: SkillPaths,
 ): RemoveResult {
@@
   return {
-    success: removedUploadDir,
+    success: removedUploadDir && (!paths.isOpenClaw || removedMirrorDir),
     removedUploadDir,
     removedMirrorDir,
     clearedSessions,
     messages,
   };
 }
@@
 export function verifyRemove(ctx: SshContext, paths: SkillPaths): boolean {
-  const target = shellQuote(paths.uploadDir);
-  const result = sshExec(ctx, `test -e ${target} && echo EXISTS || echo GONE`);
+  const checks = [`test ! -e ${shellQuote(paths.uploadDir)}`];
+  if (paths.isOpenClaw && paths.mirrorDir) {
+    checks.push(`test ! -e "${paths.mirrorDir}"`);
+  }
+  const result = sshExec(ctx, `${checks.join(" && ")} && echo GONE || echo EXISTS`);
   return result !== null && result.stdout === "GONE";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/skill-install.ts` around lines 405 - 420, The removal success
currently ignores mirror deletion; update the code so success requires the
mirror to be removed when applicable and make verifyRemove check both upload and
mirror dirs: modify the caller that builds the result object to set success =
removedUploadDir && (paths.mirrorDir ? removedMirrorDir : true) (use the
existing removedUploadDir and removedMirrorDir flags), and change the
verifyRemove(ctx: SshContext, paths: SkillPaths) function to shell-quote and
test both paths.uploadDir and paths.mirrorDir (if paths.mirrorDir is provided) —
return true only when both are gone (i.e., test each with sshExec and ensure
both echo GONE).
🧹 Nitpick comments (1)
src/lib/skill-install.ts (1)

66-70: Consolidate skill-name validation into one shared helper.

The same rule is duplicated in parseFrontmatter and validateSkillName. Centralizing it reduces drift risk and keeps install/remove behavior consistent.

Refactor sketch
+const SKILL_NAME_RE = /^[A-Za-z0-9._-]+$/;
+function isValidSkillName(name: string): boolean {
+  return name.length > 0 && name !== "." && name !== ".." && SKILL_NAME_RE.test(name);
+}
@@
-  if (nameValue === "." || nameValue === ".." || !/^[A-Za-z0-9._-]+$/.test(nameValue)) {
+  if (!isValidSkillName(nameValue)) {
@@
 export function validateSkillName(name: string): boolean {
-  return (
-    name.length > 0 &&
-    name !== "." &&
-    name !== ".." &&
-    /^[A-Za-z0-9._-]+$/.test(name)
-  );
+  return isValidSkillName(name);
 }

Also applies to: 427-433

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/skill-install.ts` around lines 66 - 70, Extract the skill-name
validation logic into a single helper (e.g., isValidSkillName or
validateSkillNameFormat) and use it from both parseFrontmatter and
validateSkillName instead of duplicating the regex and "."/".." checks;
implement the helper to return boolean or throw with the same error text
(`SKILL.md name '${name}' contains invalid characters. Only [A-Za-z0-9._-]
allowed.`) and replace the inline checks in parseFrontmatter and the existing
validateSkillName to call the new helper so install/remove behavior is
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/skill-install.ts`:
- Around line 405-420: The removal success currently ignores mirror deletion;
update the code so success requires the mirror to be removed when applicable and
make verifyRemove check both upload and mirror dirs: modify the caller that
builds the result object to set success = removedUploadDir && (paths.mirrorDir ?
removedMirrorDir : true) (use the existing removedUploadDir and removedMirrorDir
flags), and change the verifyRemove(ctx: SshContext, paths: SkillPaths) function
to shell-quote and test both paths.uploadDir and paths.mirrorDir (if
paths.mirrorDir is provided) — return true only when both are gone (i.e., test
each with sshExec and ensure both echo GONE).

---

Nitpick comments:
In `@src/lib/skill-install.ts`:
- Around line 66-70: Extract the skill-name validation logic into a single
helper (e.g., isValidSkillName or validateSkillNameFormat) and use it from both
parseFrontmatter and validateSkillName instead of duplicating the regex and
"."/".." checks; implement the helper to return boolean or throw with the same
error text (`SKILL.md name '${name}' contains invalid characters. Only
[A-Za-z0-9._-] allowed.`) and replace the inline checks in parseFrontmatter and
the existing validateSkillName to call the new helper so install/remove behavior
is consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a79c72d-a216-4456-a527-90d7ce0e709b

📥 Commits

Reviewing files that changed from the base of the PR and between 68beb05 and 36b87b1.

📒 Files selected for processing (2)
  • src/lib/skill-install.test.ts
  • src/lib/skill-install.ts

… OpenClaw mirror

- removeSkill(): success now requires both uploadDir and mirrorDir to be
  removed for OpenClaw sandboxes (previously only checked uploadDir, so
  a surviving mirror would still show the skill as removed)
- verifyRemove(): checks both uploadDir and mirrorDir via a compound SSH
  test before returning GONE (previously only checked uploadDir)
- checkExisting(): probes both uploadDir/SKILL.md and mirrorDir/SKILL.md
  so a partial removal (uploadDir gone, mirrorDir surviving) no longer
  blocks a reinstall

All 37 skill-install tests pass. Fixes address the Major issue flagged
by CodeRabbit on PR NVIDIA#1962.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/lib/skill-install.ts (1)

66-70: Consolidate skill-name validation into a single helper.

Line [66] and Line [436] duplicate the same rules. Keeping one source of truth avoids future drift between install-time and CLI-time validation.

♻️ Proposed refactor
+function isValidSkillName(name: string): boolean {
+  return (
+    name.length > 0 &&
+    name !== "." &&
+    name !== ".." &&
+    /^[A-Za-z0-9._-]+$/.test(name)
+  );
+}
+
 export function parseFrontmatter(content: string): SkillFrontmatter {
   ...
-  if (nameValue === "." || nameValue === ".." || !/^[A-Za-z0-9._-]+$/.test(nameValue)) {
+  if (!isValidSkillName(nameValue)) {
     throw new Error(
       `SKILL.md name '${nameValue}' contains invalid characters. Only [A-Za-z0-9._-] allowed.`,
     );
   }
   ...
 }
 ...
 export function validateSkillName(name: string): boolean {
-  return (
-    name.length > 0 &&
-    name !== "." &&
-    name !== ".." &&
-    /^[A-Za-z0-9._-]+$/.test(name)
-  );
+  return isValidSkillName(name);
 }

Also applies to: 436-442

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/skill-install.ts` around lines 66 - 70, Extract the duplicated
validation into a single helper called e.g. validateSkillName(name: string) (or
isValidSkillName returning boolean) that encapsulates the checks against "." and
".." and the /^[A-Za-z0-9._-]+$/ regex and the error message (`SKILL.md name
'${name}' contains invalid characters. Only [A-Za-z0-9._-] allowed.`); replace
the inline check in the install flow that uses nameValue and the duplicate check
at the CLI side (the block around the other duplicate) to call this helper
instead (either throw from validateSkillName or check boolean and throw at the
call site) so both install-time and CLI-time use the same validation logic and
message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/skill-install.ts`:
- Around line 66-70: Extract the duplicated validation into a single helper
called e.g. validateSkillName(name: string) (or isValidSkillName returning
boolean) that encapsulates the checks against "." and ".." and the
/^[A-Za-z0-9._-]+$/ regex and the error message (`SKILL.md name '${name}'
contains invalid characters. Only [A-Za-z0-9._-] allowed.`); replace the inline
check in the install flow that uses nameValue and the duplicate check at the CLI
side (the block around the other duplicate) to call this helper instead (either
throw from validateSkillName or check boolean and throw at the call site) so
both install-time and CLI-time use the same validation logic and message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 68e25d3e-e4b7-4326-9fe4-4d96807d3b6e

📥 Commits

Reviewing files that changed from the base of the PR and between 36b87b1 and b5a8c53.

📒 Files selected for processing (2)
  • src/lib/skill-install.test.ts
  • src/lib/skill-install.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/skill-install.test.ts

@sinha-abhash
Copy link
Copy Markdown
Author

✨ Thanks for submitting this PR that proposes an enhancement to add a skill remove command, which could help improve the user experience and provide a complete skill lifecycle from the CLI.

Possibly related open PRs:

Possibly related open issues:

Hi @wscurran, thanks for the review!

#1845 and #1844 are related but not duplicates — #1845 (merged) implements skill install, and #1844 is the issue it closed. Skill removal was explicitly listed as out of scope in #1844 (see the "Out of scope" section: "Skill removal or update commands (follow-up)").

This PR is that follow-up, completing the skill lifecycle:

nemoclaw <sandbox> skill install <path>   # added in #1845
nemoclaw <sandbox> skill remove <name>    # this PR

Happy to add a reference to #1845 in the PR description to make the relationship clearer if that helps.

Move validateSkillName before parseFrontmatter so parseFrontmatter
can call it instead of duplicating the same dot/double-dot and regex
checks inline. Removes the second definition at the bottom of the file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/skill-install.ts`:
- Around line 81-84: The error thrown when validateSkillName(nameValue) fails
should explicitly mention reserved-name restrictions (e.g., "." and "..") as
distinct from invalid character rules; update the message in the thrown Error
inside the SKILL.md name check to state both: allowed characters [A-Za-z0-9._-]
and that names cannot be "." or ".." (or other reserved names if applicable),
referencing validateSkillName and nameValue so maintainers can locate and adjust
the error string accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7afe3706-6ea6-456a-a029-99ba7cd28a93

📥 Commits

Reviewing files that changed from the base of the PR and between b5a8c53 and f418178.

📒 Files selected for processing (1)
  • src/lib/skill-install.ts

Comment thread src/lib/skill-install.ts
Extract skill remove logic into sandboxSkillRemove() and skill install
logic into sandboxSkillDeploy(). sandboxSkillInstall() is now a thin
dispatcher that handles help and routes to the two handlers.
Each function has a single responsibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/nemoclaw.ts`:
- Around line 1380-1382: skillInstall.checkExisting currently conflates “absent”
and “probe failed” (see src/lib/skill-install.ts:349-356); change checkExisting
to return a tri-state/result object (for example {exists: boolean|null, error?:
Error}) or throw on probe failure, and then update the caller here to
distinguish outcomes: if exists === false report the skill is not installed, if
exists === null or an error was returned/logged then surface the probe error
(console.error with the error) and exit (or propagate) instead of printing “not
installed”. Reference: skillInstall.checkExisting, variables ctx, paths,
skillName, sandboxName.
- Around line 1394-1399: The current message assumes verifyRemove succeeded in
proving deletion, but skillInstall.verifyRemove(ctx, paths) can return false on
SSH/verification failure; update the error path where gone is false (the block
using gone, skillName and paths.uploadDir) to log that removal is unverified
rather than asserting the dir still exists—e.g. change the console.error message
to indicate "could not verify removal" and include skillName and paths.uploadDir
(or include the underlying error if available from verifyRemove) so callers know
verification failed rather than deletion was proven to have failed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 798ae3cd-318f-4d60-800e-b93fbfb611b6

📥 Commits

Reviewing files that changed from the base of the PR and between f418178 and 8fa4c63.

📒 Files selected for processing (1)
  • src/nemoclaw.ts

Comment thread src/nemoclaw.ts Outdated
Comment thread src/nemoclaw.ts
- Clarify error message for invalid skill names to mention '.' and '..'
- Change checkExisting() to return boolean|null (null = SSH probe failed)
  to distinguish absent from unreachable sandbox
- Update both callers: remove exits with clear error on null; install
  treats null as fresh install with a warning
- Fix verifyRemove failure message to say 'could not be verified' instead
  of asserting the upload dir still exists

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sinha-abhash
Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/lib/skill-install.ts (1)

354-363: ⚠️ Potential issue | 🟠 Major

Preserve the tri-state on non-zero SSH exits.

sshExec() only returns null when spawnSync() throws. A normal SSH transport/auth failure still comes back with a non-zero status, so this path still collapses probe failures into false instead of null. That means install can still treat an unreachable sandbox as a fresh install, and remove can still say the skill is not installed when the probe never completed.

🩹 Proposed fix
 export function checkExisting(ctx: SshContext, paths: SkillPaths): boolean | null {
   const checks = [`test -f ${shellQuote(`${paths.uploadDir}/SKILL.md`)}`];
   if (paths.isOpenClaw && paths.mirrorDir) {
     checks.push(`test -f "${paths.mirrorDir}/SKILL.md"`);
   }
   const result = sshExec(ctx, `{ ${checks.join(" || ")}; } && echo EXISTS || echo ABSENT`);
-  if (result === null) {
+  if (result === null || result.status !== 0) {
     return null;
   }
   return result.stdout === "EXISTS";
 }

Verify by comparing sshExec() and checkExisting(): if SSH failures can produce a non-zero exit without throwing, this function should map that case to null.

#!/bin/bash
sed -n '168,200p' src/lib/skill-install.ts
printf '\n---\n'
sed -n '344,364p' src/lib/skill-install.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/skill-install.ts` around lines 354 - 363, The checkExisting function
currently treats any non-null sshExec() result as a definitive false/true, but
sshExec can return a non-zero exit/status for SSH transport or auth failures
(not just throw), so update checkExisting (referencing checkExisting, sshExec,
SshContext, SkillPaths) to detect sshExec results where result.status !== 0 (or
an error indicated in result.stderr) and return null in that case; otherwise
keep the existing stdout === "EXISTS" logic. Ensure you surface the tri-state by
returning null for SSH failures, true when stdout is "EXISTS", and false
otherwise.
src/nemoclaw.ts (1)

1353-1356: ⚠️ Potential issue | 🟡 Minor

Mention reserved names in the CLI validation error.

validateSkillName() also rejects . and .., so this message is still misleading for those inputs. Surface the reserved-name rule here too so the CLI explains the actual failure.

✏️ Suggested tweak
   if (!skillInstall.validateSkillName(skillName)) {
     console.error(`  Invalid skill name: '${skillName}'`);
-    console.error("  Skill names must match [A-Za-z0-9._-].");
+    console.error("  Skill names must use [A-Za-z0-9._-] and cannot be '.' or '..'.");
     process.exit(1);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 1353 - 1356, The CLI error for invalid skill
names is misleading because validateSkillName(skillName) also rejects the
reserved names "." and ".."; update the messages in the block that checks
validateSkillName(skillName) so the output mentions both the allowed pattern and
the reserved-name rule (e.g., state that "." and ".." are reserved and not
allowed), keeping the existing descriptive first line (`Invalid skill name:
'${skillName}'`) and replacing the second line to include the regex constraint
and the reserved names; locate this logic around the
skillInstall.validateSkillName call and adjust the console.error strings
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/skill-install.ts`:
- Around line 354-363: The checkExisting function currently treats any non-null
sshExec() result as a definitive false/true, but sshExec can return a non-zero
exit/status for SSH transport or auth failures (not just throw), so update
checkExisting (referencing checkExisting, sshExec, SshContext, SkillPaths) to
detect sshExec results where result.status !== 0 (or an error indicated in
result.stderr) and return null in that case; otherwise keep the existing stdout
=== "EXISTS" logic. Ensure you surface the tri-state by returning null for SSH
failures, true when stdout is "EXISTS", and false otherwise.

In `@src/nemoclaw.ts`:
- Around line 1353-1356: The CLI error for invalid skill names is misleading
because validateSkillName(skillName) also rejects the reserved names "." and
".."; update the messages in the block that checks validateSkillName(skillName)
so the output mentions both the allowed pattern and the reserved-name rule
(e.g., state that "." and ".." are reserved and not allowed), keeping the
existing descriptive first line (`Invalid skill name: '${skillName}'`) and
replacing the second line to include the regex constraint and the reserved
names; locate this logic around the skillInstall.validateSkillName call and
adjust the console.error strings accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1df3f5b4-0338-409f-b1c7-2d29d57409ef

📥 Commits

Reviewing files that changed from the base of the PR and between f418178 and f51e324.

📒 Files selected for processing (2)
  • src/lib/skill-install.ts
  • src/nemoclaw.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement: feature Use this label to identify requests for new capabilities in NemoClaw. NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants