From f099346d2f8e42dc6f18b55ef6418454e00a2338 Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Mon, 13 Apr 2026 18:52:23 -0700 Subject: [PATCH] fix(cli): report skipped hidden directories during skill install collectFiles() silently dropped dot-directories (e.g. .secret/) without adding them to skippedDotfiles. Users were warned about skipped dot-files but not about entire hidden directory trees being omitted. Now both dot-files and dot-directories are reported, with a trailing / to distinguish directories in the CLI output. Reported-by: brandonpelfrey Signed-off-by: Senthil Ravichandran --- src/lib/skill-install.test.ts | 16 ++++++++++++++++ src/lib/skill-install.ts | 6 +++--- src/nemoclaw.ts | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/lib/skill-install.test.ts b/src/lib/skill-install.test.ts index d46108798fb..9922d92bac1 100644 --- a/src/lib/skill-install.test.ts +++ b/src/lib/skill-install.test.ts @@ -158,6 +158,22 @@ describe("collectFiles", () => { } }); + it("reports hidden directories in skippedDotfiles", () => { + setup({ + "SKILL.md": "---\nname: safe\n---\n", + ".secret/token.txt": "secret-value", + "scripts/visible.sh": "#!/bin/sh", + "scripts/.hidden.sh": "#!/bin/sh", + }); + try { + const { files, skippedDotfiles } = collectFiles(tmpDir); + expect(files.sort()).toEqual(["SKILL.md", "scripts/visible.sh"]); + expect(skippedDotfiles.sort()).toEqual([".secret/", "scripts/.hidden.sh"]); + } finally { + cleanup(); + } + }); + it("flags files with unsafe characters", () => { setup({ "SKILL.md": "---\nname: bad\n---\n", diff --git a/src/lib/skill-install.ts b/src/lib/skill-install.ts index b84848cf96f..10abf1d7408 100644 --- a/src/lib/skill-install.ts +++ b/src/lib/skill-install.ts @@ -220,7 +220,7 @@ export function collectFiles(dir: string): CollectedFiles { for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const rel = prefix ? `${prefix}/${entry.name}` : entry.name; if (entry.name.startsWith(".")) { - if (entry.isFile()) skippedDotfiles.push(rel); + skippedDotfiles.push(entry.isDirectory() ? `${rel}/` : rel); continue; } if (entry.isDirectory()) { @@ -293,8 +293,8 @@ export function postInstall( : paths.mirrorDir; const mirrorFile = `${paths.mirrorDir}/${rel}`; // mirrorDir contains $HOME which must expand, so we use double - // quotes for the mkdir target but shellQuote the relative part - // to prevent injection from file names. + // quotes (not shellQuote). Safe because validateRelativePath + // restricts filenames to [A-Za-z0-9._-/] before we reach here. const result = sshExec( ctx, `mkdir -p "${mirrorSubdir}" && cat > "${mirrorFile}"`, diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index fd5d8ed4a6c..3f06e25491a 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -1306,7 +1306,7 @@ async function sandboxSkillInstall(sandboxName, args = []) { process.exit(1); } if (collected.skippedDotfiles.length > 0) { - console.log(` ${D}Skipping ${collected.skippedDotfiles.length} dotfile(s): ${collected.skippedDotfiles.join(", ")}${R}`); + console.log(` ${D}Skipping ${collected.skippedDotfiles.length} hidden path(s): ${collected.skippedDotfiles.join(", ")}${R}`); } const fileLabel = collected.files.length === 1 ? "1 file" : `${collected.files.length} files`; console.log(` ${G}✓${R} Validated SKILL.md (name: ${frontmatter.name}, ${fileLabel})`);