Skip to content

Commit f5cfb0a

Browse files
rubenmarcusclaude
andcommitted
fix(security): eliminate file system race condition in skills detection
Remove statSync+readFileSync TOCTOU pattern entirely. Instead of checking file type then reading, try reading directly and catch errors. This eliminates the race window between stat and read. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a521503 commit f5cfb0a

File tree

1 file changed

+20
-22
lines changed

1 file changed

+20
-22
lines changed

src/loop/skills.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
1+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
22
import { homedir } from 'node:os';
33
import { join } from 'node:path';
44

@@ -84,35 +84,33 @@ function scanSkillsDir(dir: string, source: ClaudeSkill['source']): ClaudeSkill[
8484
for (const entry of entries) {
8585
const fullPath = join(dir, entry);
8686

87-
try {
88-
const stats = statSync(fullPath);
89-
90-
if (stats.isFile() && entry.endsWith('.md')) {
91-
// Flat .md skill file
87+
if (entry.endsWith('.md')) {
88+
// Try reading as a flat .md skill file
89+
try {
9290
const content = readFileSync(fullPath, 'utf-8');
9391
skills.push({
9492
name: extractName(content, entry.replace('.md', '')),
9593
path: fullPath,
9694
description: extractDescription(content),
9795
source,
9896
});
99-
} else if (stats.isDirectory()) {
100-
// Try reading SKILL.md inside subdirectory
101-
const skillMdPath = join(fullPath, 'SKILL.md');
102-
try {
103-
const content = readFileSync(skillMdPath, 'utf-8');
104-
skills.push({
105-
name: extractName(content, entry),
106-
path: skillMdPath,
107-
description: extractDescription(content),
108-
source,
109-
});
110-
} catch {
111-
// SKILL.md not found or unreadable, skip
112-
}
97+
} catch {
98+
// File unreadable, skip
99+
}
100+
} else {
101+
// Try reading SKILL.md inside subdirectory
102+
const skillMdPath = join(fullPath, 'SKILL.md');
103+
try {
104+
const content = readFileSync(skillMdPath, 'utf-8');
105+
skills.push({
106+
name: extractName(content, entry),
107+
path: skillMdPath,
108+
description: extractDescription(content),
109+
source,
110+
});
111+
} catch {
112+
// Not a skill directory or unreadable, skip
113113
}
114-
} catch {
115-
// Skip unreadable entries
116114
}
117115
}
118116
} catch {

0 commit comments

Comments
 (0)