远程与桌面端协同:会话迁移与跨端执行协议
Claude Code 并不局限于本地终端。通过 Web 桥接、远程执行(Remote Execution)和桌面端接力(Desktop Handoff)协议,它支持将会话在不同形态的客户端之间平滑迁移。
本质
跨端协同本质上是 运行时环境与执行控制权(Control Plane)的解耦协议。
当你在本地终端启动 Claude Code 并指定 --remote(或进入 Teleported 状态)时,CLI 的角色从“独立执行引擎”转变为“远程环境的种子(Seed)或代理”。它负责将会话元数据、当前代码快照和环境配置打包,传递给更高层级的托管平台。而 /desktop(或 /app)则是这种协议在本地侧的极致应用:利用 Deep Link 将控制权移交给 Claude Desktop,通过本地文件系统进行接力。
实现机制
跨端与迁移逻辑主要分布在 claude-code-opensource/src/bootstrap/state.ts 和各级命令入口中:
远程模式标志(isRemoteMode): 当环境检测到
CLAUDE_CODE_REMOTE为真,或通过--remote显式启动时,STATE.isRemoteMode会被激活。此时,CLI 进入“受限态”:- 命令治理:通过
filterCommandsForRemoteMode禁用本地风险命令(如直接修改敏感配置)。 - 上下文剪裁:在
getSystemContext中跳过耗时的 Git Status 扫描,因为远程环境通常有自己的仓库管理系统。
- 命令治理:通过
桌面端接力(Desktop Handoff): 执行
/desktop或/app会触发claude-code-opensource/src/commands/desktop/desktop.tsx中的逻辑:- 版本前哨检查:先通过组件检查 Claude Desktop 是否安装,且版本是否满足最低要求(如
1.1.2396+)。 - 强制落盘(Sync Flush):在跳转前强制调用
flushSessionStorage(),确保所有 Transcript 状态已写入本地磁盘。这是因为桌面端并非从 CLI 内存接状态,而是基于磁盘持久化 ID 恢复。 - Deep Link 生成:利用
claude-code-opensource/src/utils/desktopDeepLink.ts拼装claude://resume?session=...&cwd=...的 URL,唤起桌面端并退出 CLI。
- 版本前哨检查:先通过组件检查 Claude Desktop 是否安装,且版本是否满足最低要求(如
会话传送与接入(Teleporting & Ingress): 当你使用云端迁移功能时,系统会触发
setTeleportedSessionInfo记录状态。- 种子信息打包:系统生成包含会话摘要和临时认证令牌的链接。
- 接入认证:利用
getSessionIngressAuthToken确保只有合法发起方能连回执行环境。 - Web 端模拟层:Web 界面加载一个与本地 CLI 协议兼容的逻辑层,解析并渲染由种子定义的工具集与指令。
别踩这些坑
- 本地性限制:
/desktop仅支持 macOS 与 Windows x64。它依赖本地会话存储,如果你期待的是把任务迁到另一台物理机器或云端后台,/desktop并非为此设计,你应该使用--remote。 - 同步与分叉风险:一旦会话被“传送到”Web 或远程端,后续历史通常保存在云端。如果你回到本地再次
--resume相同 Session,系统会尝试拉取云端 Transcript 进行同步。 - 代码种子的局限:迁移时通常只传送“初始代码快照”或“变更差异”。如果本地项目包含海量、未被追踪的二进制大文件,远程端可能无法完整复现该环境。
- 权限与策略治理:远程模式下的权限判定由云端策略(Policy Limits)接管。即便本地允许某些操作,如果云端策略限制了该 Session,执行仍会失败。
延伸阅读
- 如果你想了解云端策略如何动态限制本地执行,请看 受管配置与策略:Server-managed Settings。
- 如果你想深入了解本地 Transcript 具体的存储结构,请看 会话恢复与选择器:从本地索引到运行时重载。
- 如果你对远程环境下的网络层安全感兴趣,请看 网络栈与企业配置:受管配置详解。
源码锚点
claude-code-opensource/src/bootstrap/state.ts—setTeleportedSessionInfo传送状态的定义。
📄 src/bootstrap/state.ts — `setTeleportedSessionInfo` 传送状态的定义。
typescript
export function setTeleportedSessionInfo(info: {
sessionId: string | null
}): void {
STATE.teleportedSessionInfo = {
isTeleported: true,
hasLoggedFirstMessage: false,
sessionId: info.sessionId,
}
}claude-code-opensource/src/commands/desktop/desktop.tsx—/desktop命令入口与版本前哨检查。
📄 src/commands/desktop/desktop.tsx — `/desktop` 命令入口与版本前哨检查。
tsx
export async function call(onDone: (result?: string, options?: {
display?: CommandResultDisplay;
}) => void): Promise<React.ReactNode> {
return <DesktopHandoff onDone={onDone} />;
}claude-code-opensource/src/utils/desktopDeepLink.ts— 生成跨端跳转 Deep Link 的核心工具。
📄 src/utils/desktopDeepLink.ts — 生成跨端跳转 Deep Link 的核心工具。
typescript
function buildDesktopDeepLink(sessionId: string): string {
const protocol = isDevMode() ? 'claude-dev' : 'claude'
const url = new URL(`${protocol}://resume`)
url.searchParams.set('session', sessionId)
url.searchParams.set('cwd', getCwd())
return url.toString()
}claude-code-opensource/src/utils/sessionIngressAuth.ts— 生成跨端接入凭证与令牌的逻辑。
📄 src/utils/sessionIngressAuth.ts — 生成跨端接入凭证与令牌的逻辑。
typescript
function getTokenFromFileDescriptor(): string | null {
// Check if we've already attempted to read the token
const cachedToken = getSessionIngressToken()
if (cachedToken !== undefined) {
return cachedToken
}
const fdEnv = process.env.CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR
if (!fdEnv) {
// No FD env var — either we're not in CCR, or we're a subprocess whose
// parent stripped the (useless) FD env var. Try the well-known file.
const path =
process.env.CLAUDE_SESSION_INGRESS_TOKEN_FILE ??
CCR_SESSION_INGRESS_TOKEN_PATH
const fromFile = readTokenFromWellKnownFile(path, 'session ingress token')
setSessionIngressToken(fromFile)
return fromFile
}
const fd = parseInt(fdEnv, 10)
if (Number.isNaN(fd)) {
logForDebugging(
`CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR must be a valid file descriptor number, got: ${fdEnv}`,
{ level: 'error' },
)
setSessionIngressToken(null)
return null
}
try {