Skip to content
源码分析手册

Skills 自动发现:按需加载的路径感知技能系统

对应官方文档:claude-code-docs/docs/skills.md 里的 Automatic discovery from nested directories

核心概念

文档里的“自动发现”最容易让人脑补成:Claude 在启动时就把整个仓库下的所有 .claude/skills 都递归扫一遍。

但在源码层面,这套机制其实是一个基于文件操作路径触发的“后加载(Lazy Loading)”系统。 启动时,Claude 只加载固定位置(Enterprise、User、Project Root 等)的技能。位于子目录中的技能,只有当你读取、修改或写入某个具体文件后,系统才沿着那个文件的父目录一路往上回溯,把离它最近的技能补载进当前会话。

这种设计是 Monorepo 场景下包级 Skill(Package-level Skills)能够成立的关键:一个子包的技能,不需要在你还没碰它时就污染整个 Session 的 Context。

代码里的真实逻辑

实现这套按需发现的核心在 src/skills/loadSkillsDir.ts

  1. 技能来源分层
    • 常驻技能(Permanent):在 getSkillDirCommands(...) 阶段,系统加载 Enterprise、User、Project Root、--add-dir 和 Legacy /commands
    • 动态技能(Dynamic):通过 discoverSkillDirsForPaths(...) 触发。
  2. 路径感知与回溯(Path Backtracing):当 FileReadToolFileEditToolFileWriteTool 被调用处理具体路径时,它们都会触发发现器。回溯从目标文件的直接父目录开始,一级级向上直到 CWD,并在每一层检查是否存在 .claude/skills
  3. 负缓存与防抖(Negative Caching):为了防止反复扫描,源码会将没有技能目录的路径记入 dynamicSkillDirs 里的 miss 状态。这意味着它既是扫描器,也是带状态的路径追逐器。
  4. 深度优先覆盖(Override by Depth):发现结果按目录深度排序。在 addSkillDirectories(...) 中,新发现的技能目录按“先浅后深”写入 Map。结果是:离操作文件越近(越深)的同名技能,会最后写入并覆盖掉外层的旧版本。
  5. 按需提示注入(Just-in-time Prompting):动态发现出的技能并不会立刻将全文塞进 Prompt。初次发现时,系统只把 namedescriptionwhenToUse 注册进清单;只有模型真正调用该技能时,getPromptForCommand(...) 才会把完整的 SKILL.md 和依赖项展开。

踩坑指南

  1. 触发点局限性:只有通过文件读写工具(File Tools)访问的路径才触发自动发现。如果你只是 ls 一个目录,可能并不会激活其中的局部技能。
  2. Gitignore 过滤:源码明确规定,被 Gitignore 命中的目录不会被捞进技能发现链。这防止了 node_modules 里的第三方技能被意外带入。
  3. 同名冲突与覆盖:深度更大的 .claude/skills 目录拥有最高优先级。这意味着你可以通过在子目录放一个同名 SKILL 来“劫持”全局技能的行为。
  4. Simple Mode 差异:在这份 2.1.88 快照中,Read 和 Edit 工具在 CLAUDE_CODE_SIMPLE 模式下会显式跳过动态发现逻辑,但 Write 工具却没有同样的 Guard。如果你发现写文件时技能冒出来了但读文件时没有,这可能就是原因。

相关主题

  • 如果你关心技能真正被调用时如何渲染,下一篇该看 SkillTool & getPromptForCommand
  • 如果你更关心技能在 Monorepo 里的工程化实践,去看 .claude/skills 的最佳实践
  • 如果你想看 --bare 模式如何剪裁这套逻辑,去看 Headless 模式下的 Skill 加载链

源码锚点

  • claude-code-opensource/src/skills/loadSkillsDir.ts:动态回溯、深度排序与覆盖逻辑的主战场。
📄 src/skills/loadSkillsDir.ts — 动态回溯、深度排序与覆盖逻辑的主战场。L17-20 of 1087
typescript
  type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  logEvent,
} from '../services/analytics/index.js'
import { roughTokenCountEstimation } from '../services/tokenEstimation.js'
  • claude-code-opensource/src/tools/FileReadTool/FileReadTool.ts:读操作触发 Skill 发现的入口。
📄 src/tools/FileReadTool/FileReadTool.ts — 读操作触发 Skill 发现的入口。L24-28 of 1184
typescript
  activateConditionalSkillsForPaths,
  addSkillDirectories,
  discoverSkillDirsForPaths,
} from '../../skills/loadSkillsDir.js'
import type { ToolUseContext } from '../../Tool.js'
  • claude-code-opensource/src/tools/FileEditTool/FileEditTool.ts:编辑操作触发 Skill 发现的入口。
📄 src/tools/FileEditTool/FileEditTool.ts — 编辑操作触发 Skill 发现的入口。L10-14 of 626
typescript
  activateConditionalSkillsForPaths,
  addSkillDirectories,
  discoverSkillDirsForPaths,
} from '../../skills/loadSkillsDir.js'
import type { ToolUseContext } from '../../Tool.js'
  • claude-code-opensource/src/tools/FileWriteTool/FileWriteTool.ts:写操作触发入口(注意其 Simple Mode 处理差异)。
📄 src/tools/FileWriteTool/FileWriteTool.ts — 写操作触发入口(注意其 Simple Mode 处理差异)。L32-34 of 435
typescript
  type ToolUseDiff,
} from '../../utils/gitDiff.js'
import { lazySchema } from '../../utils/lazySchema.js'
  • claude-code-opensource/src/QueryEngine.ts:决定初始 Context 中带入哪些技能清单的裁决点。
📄 src/QueryEngine.ts — 决定初始 Context 中带入哪些技能清单的裁决点。L69-72 of 1296
typescript
  type ProcessUserInputContext,
  processUserInput,
} from './utils/processUserInput/processUserInput.js'
import { fetchSystemPromptParts } from './utils/queryContext.js'

基于 Claude Code v2.1.88 开源快照的深度分析