send-to — 把消息转达给本机另一个 Claude Code 会话
底层是 Claude Code 的跨会话消息:ListAgents 列出本机 peer 会话,SendMessage 按名字(或按 uds: 地址)发纯文本。对方收到的只有这段文本——没有本会话的任何历史、文件或上下文。
在 HAPI 会话里,SendMessage 仍是默认投递方式;HAPI 自带的 mcp__hapi__ping_peer / hapi ping-peer 可作替代通道,但绝不传 force——它拒绝的目标,force 一下就是杀掉对方正在跑的一切(见「HAPI 会话」一节)。
环境要求
- Claude Code v2.1.224+,macOS/Linux(Windows 原生不支持)。会话里
/list-agents不认识 = 该会话没有此功能。 - 相邻命令:
/list-agents(别名/peers)查看可达会话;/rename给会话起名(起过名才能claude --resume <名字>)。发消息本身没有内置斜杠命令,这正是本 skill 存在的原因。 - 跨会话发现按 profile 隔离;投递不按账号隔离(2026-08-10 三次修订,受控实验定谳):每个会话只在自己 profile 的
sessions/目录写注册文件,ListAgents 只读本 profile 的注册表,所以不同 profile 的窗口互相不可见。投递层面:SendMessage带显式地址uds:/tmp/cc-socks/<pid>.sock可跨登录账号直达,且无需互相可见——受控实验(2026-08-10,用户主持):账号 A 的窗口 ↔ 账号 B 的窗口,双方确证不同登录账号、互不在对方 ListAgents,显式地址双向投递均成功并互收回声。(此前 2026-08-08 的「不可发」定论与 2026-08-09 的混杂样本均已被此实验取代;实验方法保留在此:双方账号确证不同 + 双向回声,今后翻案照此标准。)会话在启动时即注册(可见通常在几十秒内),与它发没发过消息无关。 - 发现通道有三条,投递通道只有一条:发现——
ListAgents(本 profile 可见 peer)、身份注册表/tmp/cc-session-registry/(跨 profile 自愿自报的名片,见下方「身份注册表」一节)、/tmp/cc-socks/目录清单(兜底,只有 pid + mtime)。投递——SendMessage一条,含按名字与按uds:地址两种形态。不许由你动用任何别的手段把内容送进对方会话——绕开 SendMessage 手写对方 socket、动对方 profile 的注册/会话文件或身份注册表条目、切 CLAUDE_CONFIG_DIR 起中继会话、自行claude --resume接管对方、tmux/screen 往对方终端注键、给mcp__hapi__ping_peer/hapi ping-peer传 force、自己换 JWT 后 curl hub 的/api/sessions/<id>/messages,全在禁止之列,技术上可行也不行。SendMessage 带 uds: 地址是官方通道的合法形态,不在禁止之列。
步骤
- 加载工具:
SendMessage是 deferred 工具,先ToolSearch("select:SendMessage")加载,否则直接调用会报 InputValidationError。ListAgents无需加载。 - 找目标:按四级阶梯找,阶梯本身不是"退而求其次"——L2 是和 L1 并列的标准路径,只是查询方式不同。
- L1 —
ListAgents命中(本 profile 可见):用参数里的第一个词做大小写不敏感匹配:精确 → 前缀 → 子串。列表里的名字多是自动生成的「目录名-两位后缀」(如myapp-ec),用户往往只记得「myapp」,这是正常输入,不是错误。- 恰好 1 个命中:精确或前缀命中 → 直接继续,不必再确认;仅靠子串才命中的,发送前把命中的完整名字亮给用户确认一遍(防 "app" 误配上 "myapp-x7" 这类)。三级全空就是 0 命中,不许自行放宽成模糊/编辑距离匹配硬凑一个"命中"。
- ≥2 个命中 → 把命中候选(名字、状态、启动时间)亮给用户,用 AskUserQuestion 让用户挑一个。不许挑"最像的"替用户拍板,也不许去翻 tmux / 进程 / 对方屏幕"侦察"对方身份——列表就是全部合法信息源。
- 0 个命中 → 目标窗口若是刚开的,注册可能有几十秒延迟——先重新 ListAgents 一次再下判断,仍是 0 才进入 L2。
- L2 — 读身份注册表(标准路径):
ListAgents0 命中不等于走投无路,先读/tmp/cc-session-registry/(字段与用法见下方「身份注册表」一节),按项目名/账号/启动时间对照用户所指目标,过滤掉「对应 socket 已消失或 pid 已死」的条目(双判据,见「身份注册表」有效性一节):- 过滤后剩唯一匹配 → 直接对该条目的
uds:地址发送。这条路径和按名字发一样正常,不必犹豫、不必先跟用户反复确认"要不要试试这个"。 - 过滤后仍是同一项目多条活条目(如交互主会话 + 若干 Workflow 派生 agent 各自独立注册)→ 一律把人话身份(项目名、账号、profile、启动时间、
cmd摘要)亮给用户挑,不许替用户拍板自行挑一个发送。cmd旗标(如-p/--effort)绝不作筛除依据——2026-08-12 终审实测反证:同机全部交互主会话的 cmd 同样带--effort ultracode,拿它当硬筛选会把 100% 主会话筛掉;cmd带-p/--print的多为 headless 一次性进程,只能作为候选列表里的排序弱提示(靠后展示),不能单方过滤掉。 - 注册表里没有匹配条目(对方 profile 没装注册 hook,见「身份注册表」一节的覆盖范围声明)→ 进入 L3,这不代表目标不存在。
- 过滤后剩唯一匹配 → 直接对该条目的
- L3 — 现行地址来源阶梯(L2 落空时):按优先级尝试:
- 对方报的:对方窗口在消息里自报过地址(
<cross-session-message from="uds:...">的 from 值),照抄作to即可——这是最可靠的来源,回复场景天然具备。 - 让目标窗口先开口(目标还没跟你说过话时的首选):请用户在目标窗口里说一句「给 <你所在项目/窗口> 发条消息说你在」。对方一发过来,你就从
from=拿到了地址,用户全程不用复制粘贴任何东西,也不用理解 socket 是什么。这条对用户的认知负担最低。 - 用户给的:用户在目标窗口里跑
/list-agents或看 statusline 拿到那个窗口自己的地址,粘给你。比第 2 条多一步复制,但同样不需要用户理解 socket。 - 候选辨认(最后手段,多数情况根本走不通):
ls -lt /tmp/cc-socks/列出全部 socket,按修改时间与 pid 给用户念候选,由用户指认哪个是目标。- 先认清它的实际命中率:普通用户看不见也无法把 socket/pid 对应到自己的哪个窗口。2026-08-12 用户原话:「我看不到 socket 我也不知道」。所以除非用户独立掌握目标窗口的 pid(极少),念清单只会白白浪费一轮对话——能走 L2 或 L3 的第 1/2/3 条就不要走这条。
- 唯一还值得念清单的场景:多个候选里只有一个的创建时间与用户刚才的动作吻合(如用户说"我刚开了个窗口",而清单里只有一个 socket 是几分钟内新建的)。即便如此也只是给用户一个可否认的确认点,不是替他拍板。
- 硬禁止不变:不许自行拿 pid 去翻进程/终端侦察对方身份替用户拍板,也不许对着未指认的地址盲发。
- 对方报的:对方窗口在消息里自报过地址(
- L4 — 文件交接降级:L2、L3 全部落空,或对用户指认/自报的地址发送仍然失败 → 见下方「文件交接降级」一节。
- L1 —
- 组装消息(自包含硬规则):对方没有任何共享上下文,消息必须独立可读:
- 背景:哪个项目、什么事;
- 实质内容:分支名、提交号、路径、结论等写全,不许出现"按我们刚才说的""之前讨论的方案"这类指代;
- 期望动作:希望对方做什么、需不需要回复。
- 用户没给内容(
/send-to myapp就一个名字)→ 取当前对话里明显要转达的事;拿不准要转达什么就先问一句。
- 发送:
SendMessage({to: "<名字或 uds: 地址>", summary: "<5-10 词摘要>", message: "<正文>"})。- 首次给某个 peer 发送可能报错要求带
[ref]确认(如myapp-ec [226d36])——错误信息里给出了确切写法,照抄重发即可,不是故障。 - 用
uds:地址发送(L2/L3)时开头自报家门(你是哪个项目/profile 的窗口),因为对方同样看不见你。是否要阻塞等回声才算发送完成,按下方「回声双档」区分。
- 首次给某个 peer 发送可能报错要求带
- 如实汇报:发送成功只说明消息已发出,汇报时只可说"已发送",不许说"对方已收到/已读"。两个会话权限模式类别不一致时,默认设置下消息会在对方那里挂起(held)等对方用户批准;挂起通知是异步发回来的,可能在你汇报之后才到——所以没收到通知也不许断言"没被挂起"。收到挂起通知就如实转告用户:"消息在对方会话被挂起,需要对方窗口批准(或对方把 crossSessionInbound 设为 accept)",不要为此重发刷屏。
回声双档(用 uds: 地址发送时,L2 与 L3 的区别)
用 uds: 地址发送(L2、L3 都可能走到这一步)不是铁板一块地都要等回声——按地址是否注册表确证分两档:
- 注册表确证(三条件同时满足:①身份注册表条目存在 ②对应 socket 仍存在且其 pid 进程仍存活(
kill -0级检查) ③条目的项目/账号与用户所指目标对得上):直发不必阻塞等回声——首发仍要自报家门(哪个项目/profile 的窗口),并附一句「若你不是 <目标>,请回我一声并忽略」作为零成本纠错邀请,但不必等这句回声才算完事。投递状态汇报仍照旧只可说"已发送"(held 挂起风险与身份确证与否无关,该说照说)。 - 非确证(L3 的排除法收窄、用户口头指认候选、对方自报/用户提供但你没有独立佐证的地址):照旧强制索取回声——不是为验证通路,而是为认对人(未列出的 socket 没有名字兜底,只有回声能确认目标身份)+ 防对方侧挂起(held)静默吞消息。首发必须附「收到请回一声;若你不是 <预期目标>,请回我并忽略」。等不到回声就不得当作已送达(对方忙、消息被挂起等你批准、或地址认错了都有可能),该转 L4 文件交接就转(2026-08-09 实测:误投后对方回信纠正,零损失,说明这条规则本身有效,不是走过场)。
身份注册表
/tmp/cc-session-registry/ 是各会话在启动时自愿写下的名片,把 L3 里"只有 pid + mtime、谁也认不出"的 socket 清单翻译成人能读懂的身份,让 L2 直发成为标准路径而不是靠猜。
目录:
/tmp/cc-session-registry/,权限0700(仅本用户),由注册 hook 在不存在时创建。不放在/tmp/cc-socks/内——那是 Claude Code 自建自管的目录,塞外来文件有被清理/冲突的风险。文件:
<pid>.json,pid 与/tmp/cc-socks/<pid>.sock对齐,一会话一文件,天然避免并发写冲突。字段:
字段 含义 取不到时 pidClaude Code 主进程 pid(int) 必有(定位不到则不写文件) socket/tmp/cc-socks/<pid>.sock全路径同上 account登录账号邮箱 "unknown"profileCLAUDE_CONFIG_DIR的 basename"default"cwd会话工作目录全路径 "unknown"projectcwd 的 basename "unknown"session_idhook stdin JSON 里的 session_id "unknown"sourcehook 触发源(startup/resume/clear/compact) "unknown"cmd自身 Claude 主进程的启动命令行,用于区分交互主会话与派生/headless 进程 "unknown"registered_atISO8601 注册时间 必有 有效性过滤:条目仅当「对应 socket 仍存在 且其 pid 进程仍存活(
kill -0级检查)」时有效——读取侧必须先按此双判据过滤,不许把 socket 已消失或 pid 已死的陈旧条目当作可发地址。单看 socket 存在不够:claude 退出后 socket 文件会残留(孤儿 socket,实证:本机曾有 socket 文件已无对应进程),单判据 = 幻影条目 + 假"可发送"。读取侧展示规则:同一项目出现多条活条目时(如 Workflow 派生的 agent 各有自己的 socket 并自注册),
cmd旗标绝不作筛除依据——同机全部交互主会话的 cmd 同样带--effort类旗标,拿它当硬筛选会把主会话全部筛掉;cmd带-p/--print的多为 headless 一次性进程,只能作为候选列表里的排序弱提示(靠后展示)。一律把候选(项目、账号、启动时间、cmd)亮给用户挑,不许替用户拍板直接投给某一条,消息可能随派生进程结束而消失却误报"已发送"。清理:注册 hook 每次运行顺手删除「socket 已消失或对应 pid 已死」的条目(双判据);整个目录可随时
rm -rf,无副作用(各会话下次 SessionStart 会重建自己的条目)。性质声明:注册表是各会话自愿自报的名片,与
ListAgents的注册同性质——读注册表不是侦察。硬禁止针对的是"单方拿 pid 翻进程/终端替用户拍板"那一类动作,那条禁令原样有效,注册表不改变它。覆盖范围:只有装了本插件(或挂了对应注册 hook)的 profile,其会话才会出现在注册表里;注册表里没条目 ≠ 目标窗口不存在——查不到就退回 L3,不许因为查不到就断言"没开"。这也是为什么 hook 需要在每个 profile 都装并生效:少装一个 profile,那个 profile 的窗口就永远只能靠 L3/L4 被找到。实测补充(2026-08-12 两轮):Workflow 派生 agent 是独立 claude 进程、有自己的 socket,但项目级 settings hook 就位后派生 agent 未在注册表出现条目,污染未复现;上面「多条活条目一律亮给用户挑」的兜底规则不依赖此结论,照样保留。
停用(opt-out):
touch ~/.claude/.cc-session-registry-off即全局停用注册与清扫(hook 检测到标志就静默退出;删除该文件恢复)。注册的是账号、项目目录这类身份元数据,不想被登记就用这个开关;停用后新会话不再被登记(此前已登记的条目随其会话退出被过滤/清扫,开关本身不即时清空),本机窗口只能靠 L3/L4 被找到。
HAPI 会话(用户引用了 /sessions/,或目标是 hub 里的会话)
HAPI 从 2026-08-26 晚的版本(hapi 仓 main 6c91cfd2)起分两代行为,先弄清目标是哪一代:
| 目标会话 | 收到 hub 消息(网页/手机/ping_peer)时 |
ping_peer 不带 force 时 |
|---|---|---|
| 升级后新开的终端会话(有原生消息口) | 消息直接写进它正在跑的 Claude,终端显示「@ HAPI 网页/手机 · 用户本人❯」;不切 remote、不杀进程;写不进就只提示、消息留队(已实测) | 直接投递 |
升级前开的终端会话、HAPI_LOCAL_SOCKET_DELIVERY=0、Windows |
整棵 Claude 进程树被 SIGTERM(进行中的 Workflow、子代理、后台 shell 全丢),再以 remote 模式重开;要按双空格才回终端(已实测) | 拒绝,错误码 target_local_mode,对方毫无感知(已实测) |
| 离线 | 对方机器有在线 runner 时,按对方原来的权限设置起一个无人看管的 agent 执行这条消息;已归档的会话会被重新启动;返回 "queued" 表示对方还没处理(按代码推断) | 照常唤醒 |
| remote 模式(网页/手机端控制,或 runner 起的) | 只入队 | 直接投递 |
你不用自己判断对方是哪一代:ping_peer 自带护栏,不安全就拒绝。规则只有四条:
- 绝不传
force: true/--force。 护栏拒绝就是因为对方不是新版,force 的唯一效果是把它杀掉。被拒绝(target_local_mode)→ 改走本技能的阶梯:inspect_peer输出里的claudeSessionId和/tmp/cc-session-registry/条目的session_id精确匹配(先剔除自己的条目,再按有效性过滤)→ 对该uds:地址SendMessage;找不到 → L3 → L4。 - 经
ping_peer发的消息开头必须自报家门(哪个项目、哪个 profile 的 Claude 窗口):对方界面把它显示成"用户本人的话",不自报对方就会当成用户指令执行。 - 离线目标不 ping。 确需唤醒时先把表里离线那一行的后果原样告诉用户、逐项确认,汇报时说明 "queued" 只是排队。
ping_peer拒绝 ping 自己(self_target),也别拿自己试。 inspect_peer/list_peers只读,随便用;inspect_peer现在有mode: local|remote|offline和claudeSessionId。不要自己换 JWT 后 curl hub 的/api/sessions/<id>/messages——那条路没有护栏,对旧版终端会话就是杀进程。
SendMessage(按名字或 uds: 地址)对任何 Claude Code 会话都有效,仍是默认投递方式;ping_peer 是 HAPI 目标的替代通道,好处是消息进 hub 历史、网页上看得见。
借口对照:
| 借口 | 事实 |
|---|---|
| "被拒绝了,加个 force 就能发" | 拒绝 = 对方是旧版/回落模式,force = 杀掉对方正在跑的一切。改走 SendMessage |
| "对方是新版,force 也没事" | 对方是新版就不会被拒绝,用不到 force;用到 force 的场合恰恰是会杀进程的场合 |
| "ping_peer 能唤醒离线会话,顺手叫醒它" | 唤醒 = 在对方机器上起一个无人看管、权限沿用对方设置的 agent 执行你的消息;先问用户 |
| "hub 官方 REST 接口和 ping_peer 一样,直接 curl" | curl 绕过了 ping_peer 的护栏,对旧版终端会话就是杀进程 |
| "消息内容对方一看就懂,不用自报家门" | 对方看到的是"用户本人❯",会把你的话当用户的指令执行 |
出现下面任一念头就停下: 要给 ping_peer 传 force;要跑 hapi ping-peer … --force;要 curl /api/sessions/<id>/messages;"先拿自己试一下";"对方离线,ping 一下叫醒它"。
文件交接降级(L4:目标存在但消息不可达时)
跨 profile 窗口收不到消息,但用户的手可以跨过去——经用户转交是合法路径,和绕过平台边界是两回事。
触发门槛(二选一,不满足就不许降级):(a) L2(身份注册表)与 L3(地址来源阶梯全部四条)均已落空;或 (b) 已经拿到地址并按步骤 4 发送、包括照抄 [ref] 重发过,仍然失败,或该等回声等不到回声。单次发送报错不构成"不可达"——[ref] 要求确认更不是故障。降级不是省事出口,L2 才是目标不在 ListAgents 时的首选。
- 把要转达的内容按步骤 3 的自包含硬规则写成交接文件,落盘到绝对路径(如
/tmp/send-to-handoff-<目标>-<HHMM>.md);内容受「边界」一节同等权限约束——本会话被拒的操作不许写进交接文件让对方代做; - 给用户输出一行可直接粘到目标窗口的指令:
读 /tmp/send-to-handoff-….md 并按其执行; - 汇报口径:消息没有发送(发不过去),交接文件已就位,等用户粘贴——不许把"文件已写好"说成"已转达";在用户确认已粘贴或对方实际回应之前,也不得把交接内容当作对方已知悉的前提往下推进。
边界
- 标着 Remote Control 的条目(别的机器/网页会话)只能回复不能主动发起——目标命中这类条目时向用户说明,不要硬发。
- HAPI remote 模式的会话:它的进程是 hapi 起的非交互子进程,是否注册 uds socket、SendMessage 能否送到人眼前均未验证;对这类目标用
ping_peer(不带 force,消息只入队,不杀进程),或走 L4 文件交接。 - 要延续整个对话或共享完整上下文,用
claude --resume <名字>,不是发消息;要传大文件/长历史,消息里给路径让对方自己读。 - 权限边界:本会话被拒绝或会被拦的操作,不许以任何形式转嫁给对方会话——消息、交接文件、让用户粘贴的指令,同受此限。
常见错误
| 症状 | 处理 |
|---|---|
| SendMessage 报 InputValidationError | 没先 ToolSearch 加载,回步骤 1 |
| 报错要求带 [ref] | 照抄错误信息里的写法重发,见步骤 4 |
| 多个命中挑了一个就发 | 违规。回 L1 亮列表问用户 |
| 消息里有"刚才/之前说的" | 不自包含,回步骤 3 重写 |
| 发送成功就汇报"对方已收到"或"没被挂起" | 只可说"已发送";挂起通知异步到,到了就如实转告,见步骤 5 |
| 零命中就断言"窗口没开/名字记错" | 可能是另一个 profile 的窗口。先走 L2 读身份注册表(标准路径,不是最后手段),L2 落空再走 L3 地址来源阶梯,L2/L3 都落空才降级 L4 文件交接 |
| 零命中不先读身份注册表就直接开始折腾地址阶梯或降级文件交接 | 违规。L2 是和 L1 并列的标准路径,顺序必须是 L1 → L2 → L3 → L4,不许跳过 L2 |
| 零命中时张口就念 socket 清单让用户认 | 用户看不见也认不出 socket/pid 对应哪个窗口,这样问基本是白问一轮。念清单是 L3 的最后一条,前面还有 L2(身份注册表)和 L3 前三条(对方自报/让目标开口/用户在目标窗口取地址)没走完 |
| 注册表确证的地址也非要等回声才敢发 | 不必。三条件(条目存在+socket 存在且 pid 存活+身份对得上)都满足时不阻塞等回声,见「回声双档」;仍要自报家门+纠错邀请,只是不阻塞 |
| L2 同项目多条活条目(交互主会话 + Workflow 派生 agent 各自注册),不亮给用户就自行挑一条(如"唯一存活条目"或第一条)直接发 | 违规,且极易投给短命的派生进程——消息随该进程结束而消失,却误报"已发送"。同项目多条活条目一律亮给用户挑,不许替用户拍板,见 L2 |
把 cmd 里的 -p/--effort 等旗标当硬筛选依据来筛除候选 |
违规。2026-08-12 终审实测反证:同机全部交互主会话的 cmd 同样带 --effort ultracode,拿它筛选会把 100% 主会话筛掉。旗标只能作排序弱提示,不许作筛除依据,见「身份注册表」读取侧展示规则 |
| 把身份注册表当侦察工具而不敢用,或反过来拿它当唯一真相源断言"注册表没有=目标不存在" | 两个方向都错。读注册表是读对方自愿自报的名片,不是侦察;但没条目只说明对方 profile 没装 hook,不能断言不存在,见「身份注册表」覆盖范围声明 |
| 拿单次混杂样本当机制结论 | 2026-08-10 的教训:先前一次"成功"因目标被 ccswitch 切成同账号而不构成证据;后由受控实验(双方账号确证不同 + 双向回声)才定谳可发。下机制结论必须排混杂 |
| 对着没被用户指认/对方自报、且不属于注册表确证的 uds: 地址盲发 | 违规。非确证地址只有三条来源(对方自报/用户提供/用户指认候选)之外不许发,且必须等回声 |
| 想用 SendMessage 之外的通道把内容送进对方会话(手写 socket、动注册文件或身份注册表条目、--resume 接管、tmux 注键……) | 一律禁止,不论技术上可不可行。SendMessage 带 uds: 地址是合法官方通道;彻底不可达时走 L4 文件交接(经用户的手) |
ping_peer 被拒绝(target_local_mode)后加 force 重发,或自己 curl hub 送消息 |
违规,对方正在跑的一切会被杀掉;改走 SendMessage,见「HAPI 会话」一节 |
经 ping_peer 发的消息开头没自报家门 |
对方界面显示为"用户本人❯",会把你的话当用户指令执行;见「HAPI 会话」规则 2 |