多 Claude Code 实例任务协调:文件系统队列设计与 Bash 实现
一个人开三个 Claude Code 窗口,一个改前端,一个写后端,一个更新文档。听起来很高效,但真干起来你会发现:手动把任务描述复制到每个窗口,效率低得要命。更麻烦的是,窗口之间完全不知道对方在干什么,改同一个文件撞车了都不知道。
我试过共享文档、聊天窗口喊话、甚至用 Git 分支当信号——都不好用。最后找到一个反直觉的方案:用文件系统当任务板,让多个 Claude Code 实例自己去抢任务。
用文件当便利贴,用目录当看板
思路不复杂。Claude Code 能读写文件,多个实例跑在同一台机器上,天然共享磁盘。那就让磁盘当协作媒介。
task-queue/
├── config.yaml # 全局配置
├── workers.yaml # 角色定义
├── pending/ # 待领取
├── running/ # 执行中
├── done/ # 已完成
└── failed/ # 已失败
pending 目录放任务,谁有空谁来揭。揭走之后文件挪到 running,干完了挪到 done。跟团队用白板贴便利贴一个道理。
有人可能会问,为什么不直接上 Redis 或消息队列?一个人在自己机器上搞多实例并行,装个 Redis 就为了传任务?太重了。文件系统零依赖,Linux 开箱即用,这就够了。
任务的状态流转就一条线:
投递任务 领取 完成
pending/ ────────► running/ ────────► done/
mv 抢占 mv 归档
│
│ 失败
▼
failed/
超时未完成
running/ ─────────────► pending/ (被其他实例重新领取)

抢占靠 mv,原子性靠 rename
两个实例同时看到同一个任务,都想去领——这是经典的竞态条件。怎么保证只有一个成功?
答案在 Linux 的 rename 系统调用里。POSIX 标准明确规定:同一文件系统上的 rename 是原子操作。而 mv 在同卷上底层就是 rename。
# 实例1
mv pending/001-fix-auth.md running/001-fix-auth__frontend.md # ✅ 成功
# 实例2(几乎同时)
mv pending/001-fix-auth.md running/001-fix-auth__backend.md # ❌ 失败:No such file or directory
一个成功,一个报文件不存在——正好是我们要的互斥效果。不用加锁,不用数据库事务,一个 mv 搞定。
有个前提:pending 和 running 必须在同一个文件系统上。跨卷 mv 变成复制+删除,两步操作不原子。部署时用 df 查一下挂载点就行。
实在不放心极端情况,可以加一层 flock 文件锁做二次保护。但实测下来 rename 的原子性够可靠,flock 更像是心理安慰。
任务文件长什么样
任务就是一个 .md 文件,YAML frontmatter 声明元数据,Markdown 正文写具体要求:
---
id: "001"
title: "修复认证模块空指针问题"
type: code-fix
priority: 1
assignee: any
created: "2026-07-02T10:00:00"
timeout: 600
---
## 任务描述
检查 src/auth.c 中的 login() 函数,修复当 username 为 NULL 时的空指针崩溃问题。
## 验收标准
- NULL 输入返回错误码,不崩溃
- 添加对应单元测试
几个字段展开说下:
- type:任务类型标签,比如
code-fix、frontend、documentation。这个值必须出现在某个实例的 capabilities 列表里,否则没人能领。 - assignee:
any表示任意实例可领;写具体 ID 比如backend,就只有 backend 实例能领。 - priority:数字越小越优先。紧急任务设 1,普通任务默认 99。
- timeout:超时秒数。领取后超过这个时间还没完成,其他实例可以把任务从 running 挪回 pending 重新领取——这是防止实例崩溃导致任务卡死的保底机制。

实例身份:我是谁,我能干什么
每个 Claude Code 实例需要两样东西:身份标识(我是谁)和能力范围(我能干什么)。
身份标识有三级读取优先级:
1. 环境变量 CLAUDE_WORKER_ID ← 推荐
↓ 未设置
2. 本地文件 ~/.claude/task-queue-myid ← 适合固定身份的机器
↓ 不存在
3. 自动生成 hostname-pid ← 仅用于临时测试
第三种为什么不推荐?每次脚本调用 PID 都不同,pick.sh 领的任务,done.sh 找不到——文件名里带的 ID 对不上。
推荐做法是在 ~/.bashrc 里定义别名:
alias claude-fe='CLAUDE_WORKER_ID=frontend TASK_QUEUE_DIR=~/task-queue claude'
alias claude-be='CLAUDE_WORKER_ID=backend TASK_QUEUE_DIR=~/task-queue claude'
alias claude-docs='CLAUDE_WORKER_ID=docs TASK_QUEUE_DIR=~/task-queue claude'
之后 claude-fe 一敲,身份和能力自动就位。
角色定义集中在 workers.yaml 里:
workers:
frontend:
name: "前端工程师"
capabilities: [code-fix, frontend, unit-test]
work_dir: /home/user/project
backend:
name: "后端工程师"
capabilities: [code-fix, backend, api-design]
work_dir: /home/user/project
docs:
name: "文档工程师"
capabilities: [documentation, code-review]
work_dir: /home/user/project
角色写死在 skill 代码里不好吗?不好。角色是配置,不是逻辑。今天三个实例,明天可能五个,改 yaml 比改代码靠谱。
领取任务时,pick.sh 拿自己的 capabilities 去匹配任务的 type。type 是 frontend 的任务,只有 capabilities 里含 frontend 的实例能领。assignee 是 any 的任务,所有匹配 type 的实例都能抢;assignee 指定了具体 ID,只有那个实例能领。
核心脚本拆解
整个 skill 七个 bash 脚本,逻辑不复杂,但每个都有讲究。
common.sh——公共函数库,所有脚本都 source 它:
get_worker_id() # 三级优先级读取身份
get_my_capabilities() # 从 workers.yaml 读取能力列表
get_my_work_dir() # 读取工作目录
now_epoch() # 当前时间戳(秒)
now_iso() # ISO 格式时间戳
capabilities 的解析踩过坑。最初用 gsub(/[[:space:]]/, "") 清空格,结果连换行符也清了,“documentation” 和 “code-review” 粘成了 “documentationcode-review”。后来改成 split 按逗号拆分,只清空格和 tab,不动换行,问题解决。
pick.sh——核心领取逻辑,155 行,做了这几件事:
- 扫描 pending/ 目录的所有
.md文件 - 扫描 running/ 目录中超时的文件(当前时间 - 修改时间 > timeout)
- 对每个候选任务检查 assignee 和 type 是否匹配
- 按 priority 排序,选最小的
- 尝试
mv抢占——成功就 claimed,失败就 lost
超时任务的处理有个细节:先 mv 回 pending 原始文件名,再走正常抢占流程。这样保证了文件名的一致性。
done.sh / fail.sh——找到自己 running/ 里的任务,追加执行记录,mv 到对应目录。追加的记录包括完成时间、实例 ID、结果摘要。
status.sh——输出 JSON,包含各目录计数和本实例当前运行的任务。
list.sh——表格形式展示 pending 任务,标注哪些本实例能领。
init.sh——一键创建目录结构、配置文件和示例任务。
定时轮询:有空就干,没空等着
CronCreate 可以注册定时任务,让 Claude Code 每隔几分钟自动检查队列。但有几个限制你得知道:
第一,只在 REPL 空闲时触发。你正在和 Claude 对话的时候,轮询不会执行。所以是"有空就干,没空等着"——不是后台线程。
第二,recurring 任务 7 天自动过期。到期得重新 /task-queue start。
第三,轮询绑定当前会话,Claude Code 退出就没了。
推荐策略:自动轮询兜底 + 手动 pick 应急。日常靠轮询自动领任务,紧急任务或轮询没触发时手动 /task-queue pick。
轮询间隔我设的 300 秒(5分钟)。太短了频繁扫描没意义,太长了任务响应慢。5 分钟是个平衡点——你喝口水回来,新任务大概率已经被领走了。
实测数据
跑了两轮测试。
场景1:并发抢占
两个实例(frontend 和 backend)同时抢一个 type: code-fix, assignee: any 的任务。
| 尝试次数 | frontend 结果 | backend 结果 |
|---|---|---|
| 1 | claimed | lost |
| 2 | lost | claimed |
| 3 | claimed | lost |
每次都只有一个成功,符合预期。
场景2:超时重领
backend 领了一个任务后"消失"(不再操作),超过 timeout 后 frontend 重新领取成功。
| 时间线 | 事件 |
|---|---|
| T+0 | backend 领取任务 |
| T+600 | 超时阈值到达 |
| T+602 | frontend pick 检测到超时,mv 回 pending,再 mv 到 running |
踩坑记录
gsub(/[[:space:]]/, "")会清掉换行符——改成split+ 逐项清理。local best_file在set -u下报 unbound variable——改成local best_file=""。- 临时 ID(hostname-pid)每次调用 PID 不同——文档里明确标注"仅用于测试,生产必须用固定身份"。

什么时候用,什么时候别用
适合的场景:
- 一个人开多个 Claude Code 窗口,按功能分工并行开发
- 长期项目积累待办事项,各实例有空就领
- CI/CD 管道发现 bug,自动写文件到 pending/ 入队
- 多个文件需要 code review,分发给不同实例并行审
不适合的场景:
- 需要毫秒级响应的实时调度——文件系统轮询做不到
- 多人共用一台机器的严格互斥——加 flock 可以缓解,但本质上是单机方案
- 任务间有复杂依赖关系——当前不支持 DAG 调度,得手动控制顺序
- 需要跨机器协调——本方案仅限同一台机器的本地文件系统
边界清醒比方案完美更重要。这个方案解决的是"一个人多窗口并行"的问题,不是分布式任务调度的问题。
后续可以做什么
任务依赖是最自然的扩展——在 frontmatter 里加 depends_on: ["001"],pick 时检查前置任务是否在 done/ 里。优先级动态调整也有用,紧急任务插队。结果通知可以接 SendMessage 或系统通知。Web 可视化用一个简单 HTML 页面展示队列状态就行。跨机器支持用 Git 仓库当共享存储,push/pull 同步状态。
这些都不急。先把单机多实例跑顺了,再考虑扩展。
更多推荐



所有评论(0)