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 是一套贯穿启动阶段和交互阶段的权限更新机制:
- 路径校验与去重:无论是 CLI 传入还是
/add-dir输入,源码都会在src/commands/add-dir/validation.ts里对路径进行规范化(展开~、转绝对路径、确认存在)。一个关键细节是“冗余检查”:如果你已经允许了/repo,再去加/repo/src,源码会将其视为冗余并忽略,防止权限树过度膨胀。 - 权限更新模型:它复用了 Claude Code 统一的权限更新逻辑,生成一个
addDirectories类型的更新。- 启动时:在
src/main.tsx中,--add-dir的值被标记为cliArg来源。这决定了它是进程级的,不会自动持久化。 - 交互时:
/add-dir允许用户选择“本次生效”(session来源)或“记住目录”(localSettings来源)。
- 启动时:在
- 沙箱同步:在
src/utils/sandbox/sandbox-adapter.ts中,additionalDirectories会被并入沙箱的allowWrite集合。这直接证明add-dir带来的是模型层面的可见性和真实的操作系统级访问权。 - 上下文发现(Discovery):被加入的目录会通过权限校验。在
src/utils/claudemd.ts(加载CLAUDE.md)和src/skills/loadSkillsDir.ts(加载技能)中,系统也会读取这份额外目录列表。即使在最小化启动的--bare模式下,源码明确规定仍需尊重显式的--add-dir输入。
踩坑指南
- 它不改变 Permission Mode:把目录加进“工作边界”只是让它变得“可被讨论、可被访问”。如果你处于
not-allow模式,写入这些新目录依然会触发确认。它扩大的是边界,而不是免审额度。 - 它不是
cd的替代品:Claude 的当前进程 CWD 并没有改变。它只是多了一个(或多个)被认可的外部根。这最适合 Monorepo 外挂、引用旁系仓库或处理临时外部配置的场景。 - 持久化差异:
--add-dir随进程消失;/add-dir选择 remember 后会写进~/.claude.json;想要团队共享,目前仍需依赖CLAUDE.md里的指导而非此命令。 - 冗余保护:你不能通过重复添加子目录来绕过某些限制,权限系统会自动对目录树进行并集缩减。
延伸阅读
- 如果你想理解为什么加了目录还是会有权限提示,下一篇该看 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` 更新。
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 — 路径规范化与冗余检测逻辑。
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 合并成初始权限上下文。
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` 白名单。
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` 是极少数被保留的技能发现入口之一。
typescript
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import { roughTokenCountEstimation } from '../services/tokenEstimation.js'