@@ -194,7 +194,7 @@ func loadSkillsFlat(dir string) []Skill {
194194
195195 var skills []Skill
196196 for _ , entry := range entries {
197- if ! entry .IsDir () || isHiddenOrSymlink ( entry ) {
197+ if ! entry .IsDir () || ( isHidden ( entry ) || isSymlink ( entry ) ) {
198198 continue
199199 }
200200
@@ -210,14 +210,48 @@ func loadSkillsFlat(dir string) []Skill {
210210}
211211
212212// loadSkillsRecursive loads skills from all subdirectories (Codex format).
213+ // It tracks visited real directory paths to avoid infinite loops caused by
214+ // symlinks that form cycles.
213215func loadSkillsRecursive (dir string ) []Skill {
216+ visited := make (map [string ]bool )
217+
218+ // Resolve the root so cycles back to it are detected.
219+ if realDir , err := filepath .EvalSymlinks (dir ); err == nil {
220+ visited [realDir ] = true
221+ }
222+
223+ return walkSkillsRecursive (dir , visited )
224+ }
225+
226+ // walkSkillsRecursive walks dir for SKILL.md files, using visited to skip
227+ // directories whose real path has already been traversed.
228+ func walkSkillsRecursive (dir string , visited map [string ]bool ) []Skill {
214229 var skills []Skill
215230
216231 _ = filepath .WalkDir (dir , func (path string , d fs.DirEntry , err error ) error {
217- if err != nil || d .IsDir () {
232+ if err != nil {
233+ return nil
234+ }
235+
236+ if d .IsDir () {
237+ if path != dir && isHidden (d ) {
238+ return fs .SkipDir
239+ }
240+
241+ // Resolve and de-duplicate real directory paths to catch
242+ // cycles introduced through symlinks higher up.
243+ if path != dir {
244+ if realPath , err := filepath .EvalSymlinks (path ); err == nil {
245+ if visited [realPath ] {
246+ return fs .SkipDir
247+ }
248+ visited [realPath ] = true
249+ }
250+ }
218251 return nil
219252 }
220- if isHiddenOrSymlink (d ) || d .Name () != skillFile {
253+
254+ if d .Name () != skillFile {
221255 return nil
222256 }
223257
@@ -277,17 +311,14 @@ func parseFrontmatter(content string) (Skill, bool) {
277311 return skill , true
278312}
279313
280- // isValidSkill validates skill constraints.
281314func isValidSkill (skill Skill ) bool {
282- // Description and name is required
283- if skill .Description == "" || skill .Name == "" {
284- return false
285- }
315+ return skill .Description != "" && skill .Name != ""
316+ }
286317
287- return true
318+ func isHidden (entry fs.DirEntry ) bool {
319+ return strings .HasPrefix (entry .Name (), "." )
288320}
289321
290- // isHiddenOrSymlink returns true for hidden files/dirs or symlinks.
291- func isHiddenOrSymlink (entry fs.DirEntry ) bool {
292- return strings .HasPrefix (entry .Name (), "." ) || entry .Type ()& os .ModeSymlink != 0
322+ func isSymlink (entry fs.DirEntry ) bool {
323+ return entry .Type ()& os .ModeSymlink != 0
293324}
0 commit comments