Skip to content
源码分析手册

add-dir:动态扩展 Claude 的工作空间边界

对应官方文档:claude-code-docs/docs/commands.md 里的 /add-dir <path>cli-reference.md 里的 --add-dir

从定义开始

add-dir(包括命令 /add-dir 和启动参数 --add-dir)本质上不是“切换当前项目根目录”,也不是给 Claude 开一个特殊后门。它真正的作用是:动态地将原本不在当前工作边界(CWD/Project Root)内的目录,追加进本次会话受认可的“工作目录集合”中。

这意味着两个底层的同步放行:第一,在 Claude 的应用层权限判断里,这些目录被视为“工作区内部”;第二,在执行 Bash 命令的操作系统沙箱(Sandbox)里,这些路径会被同步加入白名单。如果只加前者,Claude 会觉得能改但 Bash 会被挡住;如果只加后者,模型会因为权限校验而在应用层不断阻断自己。add-dir 确保了这两层边界的一致性扩展。

实现细节

实现上,add-dir 是一套贯穿启动阶段和交互阶段的权限更新机制:

  1. 路径校验与去重:无论是 CLI 传入还是 /add-dir 输入,源码都会在 src/commands/add-dir/validation.ts 里对路径进行规范化(展开 ~、转绝对路径、确认存在)。一个关键细节是“冗余检查”:如果你已经允许了 /repo,再去加 /repo/src,源码会将其视为冗余并忽略,防止权限树过度膨胀。
  2. 权限更新模型:它复用了 Claude Code 统一的权限更新逻辑,生成一个 addDirectories 类型的更新。
    • 启动时:在 src/main.tsx 中,--add-dir 的值被标记为 cliArg 来源。这决定了它是进程级的,不会自动持久化。
    • 交互时/add-dir 允许用户选择“本次生效”(session 来源)或“记住目录”(localSettings 来源)。
  3. 沙箱同步:在 src/utils/sandbox/sandbox-adapter.ts 中,additionalDirectories 会被并入沙箱的 allowWrite 集合。这直接证明 add-dir 带来的是模型层面的可见性和真实的操作系统级访问权。
  4. 上下文发现(Discovery):被加入的目录会通过权限校验。在 src/utils/claudemd.ts(加载 CLAUDE.md)和 src/skills/loadSkillsDir.ts(加载技能)中,系统也会读取这份额外目录列表。即使在最小化启动的 --bare 模式下,源码明确规定仍需尊重显式的 --add-dir 输入。

踩坑指南

  1. 它不改变 Permission Mode:把目录加进“工作边界”只是让它变得“可被讨论、可被访问”。如果你处于 not-allow 模式,写入这些新目录依然会触发确认。它扩大的是边界,而不是免审额度。
  2. 它不是 cd 的替代品:Claude 的当前进程 CWD 并没有改变。它只是多了一个(或多个)被认可的外部根。这最适合 Monorepo 外挂、引用旁系仓库或处理临时外部配置的场景。
  3. 持久化差异--add-dir 随进程消失;/add-dir 选择 remember 后会写进 ~/.claude.json;想要团队共享,目前仍需依赖 CLAUDE.md 里的指导而非此命令。
  4. 冗余保护:你不能通过重复添加子目录来绕过某些限制,权限系统会自动对目录树进行并集缩减。

延伸阅读

  • 如果你想理解为什么加了目录还是会有权限提示,下一篇该看 permissions / permission modes
  • 如果你关心 Claude 为什么会自动建议你添加目录,去看 Bash 路径校验(pathValidation.ts)
  • 如果你关心这些目录下的技能如何被加载,去看 skills / dynamic discovery

源码锚点

  • claude-code-opensource/src/commands/add-dir/add-dir.tsx:交互式命令的主入口,处理 addDirectories 更新。
📄 src/commands/add-dir/add-dir.tsx — 交互式命令的主入口,处理 `addDirectories` 更新。L73-81 of 126
tsx
      type: 'addDirectories' as const,
      directories: [path],
      destination
    };

    // Apply to session context
    const latestAppState = context.getAppState();
    const updatedContext = applyPermissionUpdate(latestAppState.toolPermissionContext, permissionUpdate);
    context.setAppState(prev => ({
  • claude-code-opensource/src/commands/add-dir/validation.ts:路径规范化与冗余检测逻辑。
📄 src/commands/add-dir/validation.ts — 路径规范化与冗余检测逻辑。L12-16 of 111
typescript
export type AddDirectoryResult =
  | {
      resultType: 'success'
      absolutePath: string
    }
  • claude-code-opensource/src/utils/permissions/permissionSetup.ts:启动时如何将 CLI 参数与 Settings 合并成初始权限上下文。
📄 src/utils/permissions/permissionSetup.ts — 启动时如何将 CLI 参数与 Settings 合并成初始权限上下文。L288-317 of 1533
typescript
 * Finds all dangerous permissions from rules loaded from disk and CLI arguments.
 * Returns structured info about each dangerous permission found.
 *
 * Checks Bash permissions (wildcard/interpreter patterns), PowerShell permissions
 * (wildcard/iex/Start-Process patterns), and Agent permissions (any allow rule
 * bypasses the classifier's sub-agent evaluation).
 */
export function findDangerousClassifierPermissions(
  rules: PermissionRule[],
  cliAllowedTools: string[],
): DangerousPermissionInfo[] {
  const dangerous: DangerousPermissionInfo[] = []

  // Check rules loaded from settings
  for (const rule of rules) {
    if (
      rule.ruleBehavior === 'allow' &&
      isDangerousClassifierPermission(
        rule.ruleValue.toolName,
        rule.ruleValue.ruleContent,
      )
    ) {
      const ruleString = rule.ruleValue.ruleContent
        ? `${rule.ruleValue.toolName}(${rule.ruleValue.ruleContent})`
        : `${rule.ruleValue.toolName}(*)`
      dangerous.push({
        ruleValue: rule.ruleValue,
        source: rule.source,
        ruleDisplay: ruleString,
        sourceDisplay: formatPermissionSource(rule.source),
  • claude-code-opensource/src/utils/sandbox/sandbox-adapter.ts:证明额外目录会进入沙箱 allowWrite 白名单。
📄 src/utils/sandbox/sandbox-adapter.ts — 证明额外目录会进入沙箱 `allowWrite` 白名单。L225-245 of 986
typescript
  const allowWrite: string[] = ['.', getClaudeTempDir()]
  const denyWrite: string[] = []
  const denyRead: string[] = []
  const allowRead: string[] = []

  // Always deny writes to settings.json files to prevent sandbox escape
  // This blocks settings in the original working directory (where Claude Code started)
  const settingsPaths = SETTING_SOURCES.map(source =>
    getSettingsFilePathForSource(source),
  ).filter((p): p is string => p !== undefined)
  denyWrite.push(...settingsPaths)
  denyWrite.push(getManagedSettingsDropInDir())

  // Also block settings files in the current working directory if it differs from original
  // This handles the case where the user has cd'd to a different directory
  const cwd = getCwdState()
  const originalCwd = getOriginalCwd()
  if (cwd !== originalCwd) {
    denyWrite.push(resolve(cwd, '.claude', 'settings.json'))
    denyWrite.push(resolve(cwd, '.claude', 'settings.local.json'))
  }
  • claude-code-opensource/src/skills/loadSkillsDir.ts:证明在 --bare 模式下,--add-dir 是极少数被保留的技能发现入口之一。
📄 src/skills/loadSkillsDir.ts — 证明在 `--bare` 模式下,`--add-dir` 是极少数被保留的技能发现入口之一。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 v2.1.88 开源快照的深度分析