Skip to content

Commit 476d418

Browse files
fix(readiness-core): don't let an unreadable skills path abort the whole scan (#525)
scanSkillDirectories() only guarded the readdir() call with a prior fileExists() check, which just proves the path existed at stat() time -- it doesn't prove readdir() will succeed on it. A skills path that exists but isn't actually a directory (renamed, a broken checkout, a stray file artifact) throws ENOTDIR; an unreadable or permission- denied directory throws EPERM; a path removed between the two calls throws ENOENT. All three used to propagate out of this function uncaught, which aborts the entire audit run in loop-audit and goal-audit (both call this via findSkills) instead of just skipping that one broken path and scanning the rest. tools/mcp-server/src/resolver.ts's listSkills() already handles the identical shape of problem correctly, wrapping its readdir() in a try/catch with the same fileExists()-then-readdir() structure. Applied the same fix here. Test plan: added a regression test where .claude/skills exists as a file instead of a directory, asserting the scan completes and still finds a valid skill under a sibling path instead of throwing. Full clean rebuild + npm test: 6/6 passing. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b191cef commit 476d418

2 files changed

Lines changed: 31 additions & 4 deletions

File tree

tools/readiness-core/src/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,19 @@ export async function scanSkillDirectories(root: string): Promise<string[]> {
3939
const foundSet = new Set<string>();
4040
for (const dir of dirs) {
4141
if (!(await fileExists(dir))) continue;
42-
const entries = await readdir(dir, { withFileTypes: true });
43-
for (const e of entries) {
44-
if (e.isDirectory()) foundSet.add(e.name);
45-
if (e.isFile() && e.name === 'SKILL.md') foundSet.add('root-skill');
42+
try {
43+
const entries = await readdir(dir, { withFileTypes: true });
44+
for (const e of entries) {
45+
if (e.isDirectory()) foundSet.add(e.name);
46+
if (e.isFile() && e.name === 'SKILL.md') foundSet.add('root-skill');
47+
}
48+
} catch {
49+
// fileExists only proves the path existed at stat time -- it can still
50+
// turn out to not be a directory (ENOTDIR), be unreadable (EPERM), or
51+
// be removed between the two calls (ENOENT/TOCTOU). Any of those used
52+
// to propagate out of this function uncaught, aborting the whole
53+
// audit run in loop-audit/goal-audit instead of just skipping this one
54+
// path. Matches tools/mcp-server/src/resolver.ts's listSkills().
4655
}
4756
}
4857
return [...foundSet];

tools/readiness-core/test/index.test.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,21 @@ test('scanSkillDirectories finds skills in target directory', async () => {
7474

7575
await fs.rm(fixtureDir, { recursive: true, force: true });
7676
});
77+
78+
test('scanSkillDirectories skips a skills path that is not a directory instead of throwing', async () => {
79+
// fileExists() only proves the path existed at stat() time -- it doesn't
80+
// prove readdir() will succeed on it. A `skills` path that's actually a
81+
// file (renamed, a broken checkout, a stray artifact) used to crash the
82+
// whole scan with an uncaught ENOTDIR, aborting the entire audit run in
83+
// loop-audit/goal-audit instead of just skipping this one path.
84+
const fixtureDir = path.join(process.cwd(), '.test-fixture-not-a-dir');
85+
await fs.rm(fixtureDir, { recursive: true, force: true });
86+
await fs.mkdir(path.join(fixtureDir, '.claude'), { recursive: true });
87+
await fs.writeFile(path.join(fixtureDir, '.claude', 'skills'), 'not actually a directory');
88+
await fs.mkdir(path.join(fixtureDir, 'skills', 'bar'), { recursive: true });
89+
90+
const skills = await scanSkillDirectories(fixtureDir);
91+
assert.ok(skills.includes('bar'), 'A valid skills dir is still scanned despite a broken sibling path');
92+
93+
await fs.rm(fixtureDir, { recursive: true, force: true });
94+
});

0 commit comments

Comments
 (0)