
字节笔记本
2026年10月6日 · 约 26 分钟读完
Parcels 源码拆解:293 行 Bash 的工程美学
当所有人都在用 Electron 套壳、用 Go/Rust 重写一切、把工具做成 SaaS 的时候,有人用 293 行 Bash 解决了"AI Agent 跑到一半,我要合上笔记本走人"这个 2026 年最真实的痛点。
这篇文章逐层拆解 Parcels 的源码,看它怎么用 Unix 老工具拼出一个新世界,以及我们能从中学到什么工程思维。
一、先看全貌:整个项目就是个 Bash 脚本
Clone 下来一看,整个 repo 的核心文件加起来不到 500 行:
parcels/
├── bin/parcel ← 293 行 Bash, 核心全部在这里
├── install.sh ← 28 行, 把这个脚本塞进 PATH
├── skills/parcel/SKILL.md ← 60 行, 给 AI agent 看的使用说明
└── README.md ← 69 行没有 package.json、没有 go.mod、没有 Dockerfile、没有 server、没有 database。 整个产品就是一个 Bash 脚本加一份给 AI 看的说明书。
这本身就传递了一个信号:作者把工程复杂度压到了最低。在一个所有人都在造轮子的时代,这种克制本身就是一种美学。
它的命令面只有 5 个子命令:
| 子命令 | 作用 |
|---|---|
parcel send <target> | 打包当前 repo + agent 会话,发到远端 tmux 跑 |
parcel status [target] | 看远端 tmux 里在跑啥 |
parcel doctor <target> | 检查远端环境是否就绪 |
parcel auth <target> | 同步 agent 凭证到远端 |
parcel targets | 列出已配置的目标机器 |
简单到不需要 --help 都能猜怎么用。
二、它到底解决了什么问题
在拆源码之前,先讲清楚它解决的真实痛点。
2026 年,主流的 AI 编程 agent(Claude Code、Codex CLI、Cursor Agent、Droid)都有个共同的尴尬:
| 问题 | 后果 |
|---|---|
| 跑在笔记本上,靠终端进程活着 | 一合盖子 / 一关机,Agent 就死了 |
| 长任务动辄几十分钟到几小时 | 你得干等着,没法离开 |
| 笔记本要带着走 | 在咖啡店跑一半,回家路上全没了 |
| 上云(EC2/容器)又要配 SSH、配环境、配密钥 | 上手成本高 |
本质上是缺一个"持久化执行环境"。 Parcels 的回答是:你家里那台一直开着的台式机,就是你的私人 Agent 云。

三、源码逐层拆解
我把 293 行分成 4 个概念层来讲。
第 1 层:目标管理(极简配置)
目标机器的配置就是 ~/.parcels/targets/<name>.conf,一个被 source 进来的 bash 片段:
# ~/.parcels/targets/desktop.conf
HOST="desktop" # SSH 主机名(就是 Tailscale 别名)
DEST_BASE="ai" # 远端存放目录(相对 $HOME)
AGENT="claude" # 默认 agent读取它的函数:
load_target() {
local name="$1"
local conf="$TARGETS_DIR/$name.conf"
[[ -f "$conf" ]] || die "unknown target '$name'"
HOST="" DEST_BASE="ai" AGENT="claude"
source "$conf" # 直接 source, 零依赖
[[ -n "$HOST" ]] || die "target '$name' has no HOST"
}设计要点:用 bash 自己的 source 当配置解析器,不引入 YAML/TOML 依赖。极简,但有代价:配置文件能执行任意代码(信任前提是"你能编辑这个文件")。
这种"用语言自带特性当配置格式"的选择,是 Unix 工具的常见做法(比如 git config 也是源码风格的 ini)。
第 2 层:Agent 会话定位(最巧妙的设计)
这是整个脚本里最聪明的部分。
每个 AI agent 把会话历史存在不同地方,命名规则也不同。Parcels 必须能找到"当前 repo 对应的会话文件":
claude_dir() { echo "$HOME/.claude/projects/$(echo "$1" | sed 's/[^A-Za-z0-9]/-/g')"; }
pi_dir() { echo "$HOME/.pi/agent/sessions/-$(echo "$1" | tr '/' '-')--"; }
droid_dir() { echo "$HOME/.factory/sessions/$(echo "$1" | tr '/' '-')"; }看清楚它在干嘛:
- Claude Code 把
/Users/dev/myproject这种路径里的非字母数字全部替换成-,变成-Users-dev-myproject,作为目录名。 - pi 用
tr '/' '-',把斜杠转横杠,再加前后缀。 - droid 类似。
这是反向工程了三家 AI agent 的内部存储规则。 没有调用任何 API,纯靠路径字符串推导:cheap、跨版本稳定、零依赖。
newest_session_jsonl() 找最新的会话文件:
newest_session_jsonl() {
ls -t "$1"/*.jsonl 2>/dev/null \
| grep -E '/[0-9a-f]{8}-[0-9a-f-]{27}\.jsonl$' \
| head -1
}ls -t 按时间倒序,grep 匹配 UUID 文件名格式,取最新。如果你不指定 --session,它就用这条规则自动挑最近的会话迁移。
第 3 层:四个 ship_* 函数(会话移植)
每个 agent 一个移植函数,干三件事:定位 → rsync 到远端 → 拼出 RESUME_CMD。
以 ship_claude 为例:
ship_claude() {
# 1. 定位
local sdir; sdir="$(claude_dir "$REPO")"
local sfile
if [[ -n "$SESSION_ID" ]]; then sfile="$sdir/$SESSION_ID.jsonl"
else sfile="$(newest_session_jsonl "$sdir")"; fi
local sid; sid="$(basename "$sfile" .jsonl)"
# 2. 推到远端对应位置
local rdir; rdir="$(claude_rdir "$REMOTE_REPO")"
ssh "$HOST" "mkdir -p '$rdir'"
rsync -a "$sfile" "$HOST:$rdir/"
# 3. 预信任目录, 跳过 trust dialog(否则 tmux 里会卡住)
ssh "$HOST" "repo='$REMOTE_REPO' python3 - <<'PYEOF'
import json, os
p = os.path.expanduser('~/.claude.json')
cfg = json.load(open(p)) if os.path.exists(p) else {}
proj = cfg.setdefault('projects', {}).setdefault(os.environ['repo'], {})
proj['hasTrustDialogAccepted'] = True
json.dump(cfg, open(p, 'w'))
PYEOF"
# 4. 拼启动命令
RESUME_CMD="claude --resume $sid --dangerously-skip-permissions $(printf '%q' "$PROMPT")"
}注意第 3 步,这是工程师才会想到的细节:
Claude Code 在新目录首次启动会弹"是否信任此目录"的对话框。在 tmux 非交互场景下,这个对话框会永久卡住。所以脚本通过改写 ~/.claude.json 的 hasTrustDialogAccepted: True 来绕过。
这种"挖坑填坑"的细节决定了工具好不好用。如果漏了这一步,用户发出去的 agent 99% 会在远端卡死等输入。
RESUME_CMD 用了 --dangerously-skip-permissions,这是 Claude Code 的无人值守模式,跳过所有权限确认。牺牲安全换无人值守,这是明确的取舍。
ship_droid 还要额外改一个 sessions-index.json,因为 droid 不像 claude 靠目录结构找会话,而是靠一个 JSON 索引,所以 parcels 要"伪造"一条索引记录。这种每家 agent 都要单独适配就是这类工具的工程成本。
第 4 层:cmd_send 主流程(编排所有步骤)
这是总指挥,分 4 步。
Step 1:生成 HANDOFF.md(整个项目最有思想的设计)
local snap="## Machine state (auto-generated by parcel — do not edit)
- shipped: $(date '+%Y-%m-%d %H:%M %Z') from $(hostname -s):$REPO
- target: $name → $REMOTE_REPO (agent: $AGENT)
- branch: $(git -C "$REPO" branch --show-current 2>/dev/null)
- last commit: $(git -C "$REPO" log --oneline -1 2>/dev/null)
\`\`\`
$(git -C "$REPO" status --short | head -50)
\`\`\`"HANDOFF.md 是个"半结构化"的交接文档:
- 上半部分(人工或 agent 写的):意图、进度、下一步,会被保留。
- 下半部分(脚本生成的机械快照):分支、commit、git status,每次 send 都覆盖。
它用 Python 做"半结构化合并",找到 ## Machine state (auto-generated 标记,只替换它之后的内容,保留标记之上的内容。
这等于在 repo 里维护了一个"人机共同维护的状态文件"。机器写机器的部分,人写人的部分,互不干扰。这是真正的"human-in-the-loop"工程化。
默认的 prompt 就是让远端 agent 读这个文件:
DEFAULT_PROMPT="You have been handed off from another machine.
Read HANDOFF.md at the repo root, verify the state it describes
(git status, running processes), and continue the work."Step 2:rsync 整个工作树
rsync -az --delete \
--exclude node_modules --exclude .venv --exclude venv \
--exclude __pycache__ --exclude .DS_Store --exclude .next \
--exclude target/debug --exclude target/release \
"$REPO/" "$HOST:$REMOTE_REPO/"关键细节:
--delete:远端保持和本地完全一致。重发 = 完全覆盖。--exclude列表:精心挑选了大体积、可重建的目录(node_modules/.venv/target)。- 不排除
.git、不排除.env、不排除未跟踪文件,这些是"工作状态",必须带过去。
SKILL.md 里专门警告:"整个工作树都会传,包括 .env,发到共享机器前要小心 secret"。
Step 3:调用对应的 ship_* 移植会话
RESUME_CMD=""
case "$AGENT" in
claude) ship_claude ;;
codex) ship_codex ;;
pi) ship_pi ;;
droid) ship_droid ;;
*) die "unknown agent '$AGENT'" ;;
esac如果未来要加第 5 家 agent,只需要写一个新的 ship_xxx 函数加对应的路径推导函数,主流程不动。扩展点清晰。
Step 4:在 tmux 里启动(最巧妙的工程处理)
local tname="parcel-$repo_name"
ssh "$HOST" "mkdir -p '$REMOTE_REPO/.parcel'"
# 把启动命令写成一个脚本文件, 而不是直接塞进 tmux 命令
printf '#!/usr/bin/env bash\ncd %q || exit 1\n%s\n' "$REMOTE_REPO" "$RESUME_CMD" \
| ssh "$HOST" "cat > '$REMOTE_REPO/.parcel/launch.sh' && chmod +x '$REMOTE_REPO/.parcel/launch.sh'"
# 用 tmux detached 模式启动
ssh "$HOST" "tmux kill-session -t '$tname' 2>/dev/null; \
tmux new-session -d -s '$tname' \"bash -lc '$REMOTE_REPO/.parcel/launch.sh'\""
# 等 3 秒, 检查 tmux 是否还活着(朴素的健康检查)
sleep 3
local alive
alive="$(ssh "$HOST" "tmux ls 2>/dev/null | grep -c '^$tname:' || true")"
[[ "$alive" == "1" ]] && echo "✓ running" || die "tmux session did not survive"这里有三个值得点出的设计:
-
不直接
tmux new-session "...命令...",而是先写成launch.sh文件。为什么?因为 RESUME_CMD 里有多层引号嵌套(命令里嵌 prompt 字符串),直接塞进tmux new "..."会有引号转义地狱。写成文件再bash launch.sh,引号问题彻底绕开。这是 shell 老手的经验之选。 -
tmux new-session -d:detached 模式,启动后不 attach。这是"后台跑"的关键。 -
sleep 3加存活检查:朴素但有效的"健康检查",没有真正的进程监控,但能 catch "启动即崩"的情况。
四、status 和 doctor 的巧妙之处
status:用 tmux 当通用可视化层
ssh "$HOST" '
tmux ls | grep "^parcel-" | while IFS=: read -r s _; do
echo " [$s] — last output:"
tmux capture-pane -p -t "$s" 2>/dev/null | grep -v "^$" | tail -8
done
'核心是 tmux capture-pane -p,把 tmux 面板当前内容抓成文本。
非常聪明:它把 tmux 当成了一个"通用的 agent 状态可视化层"。无论你跑的是 claude / codex / 任何 CLI,只要它输出到终端,capture-pane 就能抓到。 不需要 agent 配合、不需要日志文件、不需要 API。
doctor:远端体检
ssh "$HOST" '
for b in tmux rsync git jq claude codex pi droid; do
command -v "$b" >/dev/null 2>&1 && echo " $b: ok" || echo " $b: MISSING"
done
# 检查各家 auth 文件
[[ -f ~/.claude/.credentials.json ]] && echo "claude auth: ok" || ...
'降低用户心智负担的关键:"我发了过去为啥没跑起来?"跑一下 doctor 就知道缺啥。
五、设计哲学:为什么是 Bash,为什么这么薄
读完源码,能提炼出几条设计原则:

1. 最大化复用现有基础设施
| 需求 | 别人怎么干 | Parcels 怎么干 |
|---|---|---|
| 跨网络连接 | 自建 RPC/服务器 | Tailscale(已存在) |
| 文件传输 | 自定义协议 / 容器镜像 | rsync over SSH |
| 持久化执行 | 自研 daemon / 进程管理器 | tmux |
| Agent 状态可见 | 集成日志/可观测 SDK | tmux capture-pane |
| 配置 | YAML/TOML + 解析库 | source 一个 .conf |
| 会话定位 | 调 agent 的 API | 反向工程文件命名规则 |
每一步都选了"已存在 + 已被信任"的工具。 这是 Unix 哲学的极致体现。
2. 复杂度集中到一个扩展点
复杂度被压到了"各家 agent 的差异适配"这一个地方。如果未来加第 5 家 agent,主流程不动,只加一个 ship_xxx 函数。
3. 优雅降级
--idle:不启动,只搬运,降级成纯同步工具。--session:可选指定,不指定就自动找最新。- doctor:不假定环境完美,主动检查、给修复建议。
4. 安全上的明确取舍
| 取舍 | 解释 |
|---|---|
--dangerously-skip-permissions | 无人值守模式,agent 能做任何事。换"无人值守"的代价。 |
.env 和未跟踪文件全传 | "工作状态"优先于"安全卫生"。SKILL.md 明确警告。 |
重发 --delete 覆盖远端 | 简单一致,但远端 agent 已写的新文件会被抹掉。 |
| source conf 可执行任意代码 | 配置文件 = shell 脚本,等于信任。 |
这不是缺点,是明确的选择,把"安全"留给 Tailscale 的设备信任模型和用户自己。
六、能挑刺的地方(源码 Review)
如果我在 PR review 里挑刺,会指出这些:
- HANDOFF.md 标记匹配硬编码英文:用户翻译成中文,匹配失败导致重复 append。脆弱。
- Codex 的 session 查找 grep
"cwd":"$REPO":路径含特殊字符会错。 sleep 3是魔法数字:agent 冷启动可能要 5-10s,可能误报"启动失败"。应该改成轮询。- 并发安全:两个
parcel send同时发同一 repo 会 race。没加锁。 - 反向工程的命名规则可能随版本变:claude 升级一次,路径推导就可能对不上。
printf '%q' "$PROMPT":prompt 含%会被 printf 解释。
这些都是小问题,不影响"它是个精巧的 300 行工具"的判断。
七、和其他方案的对比
| 方案 | 优势 | 劣势 |
|---|---|---|
| Parcels | 数据不出家门、零云成本、复用现有机器 | 依赖家里机器常开、调试不方便 |
| Cursor Background Agents | 完全托管、可观测好 | 代码上云、收费、隐私 |
| Devin | 全自动、可多人看 | 贵、黑盒 |
| 自建 EC2 + tmux | 弹性、全球可达 | 要付云费、要配 VPC |
Parcels 的独特定位:"家庭云",用已有硬件、已有 Tailscale,搭出私人 Agent 托管。
八、我们能学到什么
1. 不是所有问题都需要造轮子
"AI agent 远程执行"听起来很复杂,但拆开看:连通(Tailscale)+ 传输(rsync)+ 持久化(tmux)+ 适配(脚本)。每个子问题都有几十年历史的成熟方案,拼起来就是一个产品。
2. 工程美感:把复杂度压到正确的位置
Parcels 把所有复杂度压到了"各家 agent 的差异适配"。其他部分都是薄薄一层胶水。这种"复杂度分层"是好工程的标志。
3. 半结构化数据是人机协作的好工具
HANDOFF.md 的设计:机器写机械状态、人写意图、用标记区分,是个可复用的模式。任何"人机共同维护"的文件都可以借鉴。
4. 信任是工程取舍
source conf、.env 全传、--dangerously-skip-permissions,这些都是"假设你信任环境"的选择。不是缺点,是明确地把安全责任留给更合适的层(Tailscale、用户)。
九、一句话总结
Parcels 是一个 293 行的 Bash 脚本,它把"AI agent 的远程持久化执行"这个问题,分解成了 Tailscale(连通)+ rsync(传输)+ tmux(持久化)+ 反向工程的 agent 会话定位(移植)四个已存在基础设施的组合,所有复杂度都被压缩到了"各家 agent 的差异适配"这一个扩展点上。
它的工程美感在于:没有造任何新轮子,但精准命中了一个真实痛点。
在一个所有人都在用 Electron 套壳、用 Go/Rust 重写一切的时代,这种"用最少的代码、最老的工具、解决最具体的问题"的克制,本身就是一种美学。
顺带一提:这个 Bash 版本也有一个用 Go + Wails 重写的 Parcels-Go,把带界面的单二进制带进同一场景,两种形态的取舍值得对照着看。



