MCPcopy
hub / github.com/codeaashu/claude-code / discoverSkillDirsForPaths

Function discoverSkillDirsForPaths

src/skills/loadSkillsDir.ts:861–915  ·  view source on GitHub ↗
(
  filePaths: string[],
  cwd: string,
)

Source from the content-addressed store, hash-verified

859 * @returns Array of newly discovered skill directories, sorted deepest first
860 */
861export async function discoverSkillDirsForPaths(
862 filePaths: string[],
863 cwd: string,
864): Promise<string[]> {
865 const fs = getFsImplementation()
866 const resolvedCwd = cwd.endsWith(pathSep) ? cwd.slice(0, -1) : cwd
867 const newDirs: string[] = []
868
869 for (const filePath of filePaths) {
870 // Start from the file's parent directory
871 let currentDir = dirname(filePath)
872
873 // Walk up to cwd but NOT including cwd itself
874 // CWD-level skills are already loaded at startup, so we only discover nested ones
875 // Use prefix+separator check to avoid matching /project-backup when cwd is /project
876 while (currentDir.startsWith(resolvedCwd + pathSep)) {
877 const skillDir = join(currentDir, '.claude', 'skills')
878
879 // Skip if we've already checked this path (hit or miss) — avoids
880 // repeating the same failed stat on every Read/Write/Edit call when
881 // the directory doesn't exist (the common case).
882 if (!dynamicSkillDirs.has(skillDir)) {
883 dynamicSkillDirs.add(skillDir)
884 try {
885 await fs.stat(skillDir)
886 // Skills dir exists. Before loading, check if the containing dir
887 // is gitignored — blocks e.g. node_modules/pkg/.claude/skills from
888 // loading silently. `git check-ignore` handles nested .gitignore,
889 // .git/info/exclude, and global gitignore. Fails open outside a
890 // git repo (exit 128 → false); the invocation-time trust dialog
891 // is the actual security boundary.
892 if (await isPathGitignored(currentDir, resolvedCwd)) {
893 logForDebugging(
894 `[skills] Skipped gitignored skills dir: ${skillDir}`,
895 )
896 continue
897 }
898 newDirs.push(skillDir)
899 } catch {
900 // Directory doesn't exist — already recorded above, continue
901 }
902 }
903
904 // Move to parent
905 const parent = dirname(currentDir)
906 if (parent === currentDir) break // Reached root
907 currentDir = parent
908 }
909 }
910
911 // Sort by path depth (deepest first) so skills closer to the file take precedence
912 return newDirs.sort(
913 (a, b) => b.split(pathSep).length - a.split(pathSep).length,
914 )
915}
916
917/**
918 * Loads skills from the given directories and merges them into the dynamic skills map.

Callers 3

callFunction · 0.85
callFunction · 0.85
callFunction · 0.85

Calls 6

getFsImplementationFunction · 0.85
isPathGitignoredFunction · 0.85
logForDebuggingFunction · 0.85
hasMethod · 0.45
addMethod · 0.45
pushMethod · 0.45

Tested by

no test coverage detected