欢迎光临
我们一直在努力

让 Linux 上的 Claude Code/codex 拿到 Windows 截图的小工具

让 Linux 上的 Claude Code/codex 拿到 Windows 截图的小工具

概述

picopaste 让 Windows 上的截图,经由用户已有的 ssh 通道,送达 Linux 上等待读取。 仓库地址:https://github.com/DeguiLiu/picopaste,MIT 许可。

这个工具诞生的原因

典型的开发环境是:编辑器、终端、构建流程与 Claude Code 全部运行在一台 Linux 主机上,而操作界面是 Windows,截图工具自然也属于 Windows。

由此产生的问题相当具体:刚截取的图片,如何让 Linux 一侧的 Claude Code 读取到。

目前的常见做法有三类:

  • 手工执行 scp 上传,再在终端中重新输入完整路径;
  • 将截图写入共享目录或挂载盘,再从 Linux 一侧读取;
  • 借助中转工具传递,再从终端中重新获取。

每一步本身都不复杂,但每一次都需要将注意力从当前任务切换到文件搬运上。单日重复十余次,累积的时间与注意力开销相当可观。

picopaste 将该操作压缩为一次热键:截图进入剪贴板后按下 alt+shift+v,图片即上传至 Linux,远端的绝对路径随即被写入剪贴板并输入到当前焦点窗口。Claude Code 依据该路径即可读取图片,无需任何后续操作,路径已位于光标位置。

开始使用

获取 release 中的 picopaste.exe(或自行构建),在其同级目录放置 picopaste.ini:

[picopaste]
host = user@your-linux-host
hotkey = alt+shift+v

随后运行。托盘图标出现并立即开始连接:最多短暂显示黄色,连接成功后为绿色。此后截取图片并按下热键即可。

远端实际部署了什么

既不需要自研服务端,也不需要常驻 agent,更不需要额外开放端口。使用的就是 stock sshd 及其自带的 sftp-server 子系统——绝大多数 Linux 开发机默认已经开启。

本地同样只有一个 Windows 进程。它不实现 SSH 协议,仅驱动系统中已有的 ssh:

ssh -o ClearAllForwardings=yes -s <host> sftp

-s 用于请求 sftp 子系统,stdin/stdout 随之成为 SFTP 字节流,全程不经过远端 shell。认证、密钥、ssh-agent、known_hosts 与 ~/.ssh/config 全部交由系统 OpenSSH 处理。该工具不接触任何凭据,默认也不会修改 ~/.ssh/config。

ClearAllForwardings=yes 是一项必要的细节:它屏蔽 ssh 配置中可能存在的端口转发,因为该通道只需要一个 sftp 子系统,不需要附带建立隧道。

整体链路如下。用户侧始终只有一个进程,远端一侧没有引入任何新组件:

#mermaid-svg-pRqCBUID9jdpYeAb{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pRqCBUID9jdpYeAb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pRqCBUID9jdpYeAb .error-icon{fill:#552222;}#mermaid-svg-pRqCBUID9jdpYeAb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pRqCBUID9jdpYeAb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pRqCBUID9jdpYeAb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pRqCBUID9jdpYeAb .marker.cross{stroke:#333333;}#mermaid-svg-pRqCBUID9jdpYeAb svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pRqCBUID9jdpYeAb p{margin:0;}#mermaid-svg-pRqCBUID9jdpYeAb .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pRqCBUID9jdpYeAb .cluster-label text{fill:#333;}#mermaid-svg-pRqCBUID9jdpYeAb .cluster-label span{color:#333;}#mermaid-svg-pRqCBUID9jdpYeAb .cluster-label span p{background-color:transparent;}#mermaid-svg-pRqCBUID9jdpYeAb .label text,#mermaid-svg-pRqCBUID9jdpYeAb span{fill:#333;color:#333;}#mermaid-svg-pRqCBUID9jdpYeAb .node rect,#mermaid-svg-pRqCBUID9jdpYeAb .node circle,#mermaid-svg-pRqCBUID9jdpYeAb .node ellipse,#mermaid-svg-pRqCBUID9jdpYeAb .node polygon,#mermaid-svg-pRqCBUID9jdpYeAb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pRqCBUID9jdpYeAb .rough-node .label text,#mermaid-svg-pRqCBUID9jdpYeAb .node .label text,#mermaid-svg-pRqCBUID9jdpYeAb .image-shape .label,#mermaid-svg-pRqCBUID9jdpYeAb .icon-shape .label{text-anchor:middle;}#mermaid-svg-pRqCBUID9jdpYeAb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pRqCBUID9jdpYeAb .rough-node .label,#mermaid-svg-pRqCBUID9jdpYeAb .node .label,#mermaid-svg-pRqCBUID9jdpYeAb .image-shape .label,#mermaid-svg-pRqCBUID9jdpYeAb .icon-shape .label{text-align:center;}#mermaid-svg-pRqCBUID9jdpYeAb .node.clickable{cursor:pointer;}#mermaid-svg-pRqCBUID9jdpYeAb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pRqCBUID9jdpYeAb .arrowheadPath{fill:#333333;}#mermaid-svg-pRqCBUID9jdpYeAb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pRqCBUID9jdpYeAb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pRqCBUID9jdpYeAb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pRqCBUID9jdpYeAb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pRqCBUID9jdpYeAb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pRqCBUID9jdpYeAb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pRqCBUID9jdpYeAb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pRqCBUID9jdpYeAb .cluster text{fill:#333;}#mermaid-svg-pRqCBUID9jdpYeAb .cluster span{color:#333;}#mermaid-svg-pRqCBUID9jdpYeAb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pRqCBUID9jdpYeAb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pRqCBUID9jdpYeAb rect.text{fill:none;stroke-width:0;}#mermaid-svg-pRqCBUID9jdpYeAb .icon-shape,#mermaid-svg-pRqCBUID9jdpYeAb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pRqCBUID9jdpYeAb .icon-shape p,#mermaid-svg-pRqCBUID9jdpYeAb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pRqCBUID9jdpYeAb .icon-shape .label rect,#mermaid-svg-pRqCBUID9jdpYeAb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pRqCBUID9jdpYeAb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pRqCBUID9jdpYeAb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pRqCBUID9jdpYeAb :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-pRqCBUID9jdpYeAb .win>*{fill:#1f3a5f!important;stroke:#4a90d9!important;color:#e8f0fa!important;}#mermaid-svg-pRqCBUID9jdpYeAb .win span{fill:#1f3a5f!important;stroke:#4a90d9!important;color:#e8f0fa!important;}#mermaid-svg-pRqCBUID9jdpYeAb .win tspan{fill:#e8f0fa!important;}#mermaid-svg-pRqCBUID9jdpYeAb .lnx>*{fill:#1f4a2f!important;stroke:#4aa96c!important;color:#e8faee!important;}#mermaid-svg-pRqCBUID9jdpYeAb .lnx span{fill:#1f4a2f!important;stroke:#4aa96c!important;color:#e8faee!important;}#mermaid-svg-pRqCBUID9jdpYeAb .lnx tspan{fill:#e8faee!important;}#mermaid-svg-pRqCBUID9jdpYeAb .ext>*{fill:#4a3a1f!important;stroke:#d9a441!important;color:#faf3e8!important;}#mermaid-svg-pRqCBUID9jdpYeAb .ext span{fill:#4a3a1f!important;stroke:#d9a441!important;color:#faf3e8!important;}#mermaid-svg-pRqCBUID9jdpYeAb .ext tspan{fill:#faf3e8!important;}

Linux 远端 · 零代码

Windows 客户端 · 单进程 picopaste.exe

SFTP OPEN/WRITE/CLOSE/STAT/RENAME

Ctrl+Shift+V 粘贴路径

读取

收容 ssh.exe

全局热键 · RegisterHotKey MOD_NOREPEAT

主线程消息循环 · GetMessageW(阻塞,无定时器)

托盘 · Shell_NotifyIconW + TaskbarCreated 重挂

剪贴板采集 · PNG 快路径 / DIB → WIC

上传 worker · SFTP 状态机

焦点守卫 + SendInput · 核对注入事件数

Job Object · KILL_ON_JOB_CLOSE + MEMORY_LIMIT

sshd · sftp-server 子系统

/tmp/picopaste(默认,客户端经 SFTP 创建)

Claude Code · 按路径读取图片

为什么不直接使用 scp

这一问题被反复提出,设计文档中给出了正面回答:

  • scp 在 OpenSSH 9.0 及以上版本默认走 SFTP 子系统,远端路径不再经过 shell 展开,~ 与引号会失效;
  • ssh 'cat > path' 依赖远端具备 sh、mkdir、cat、wc 等一系列命令,且需手工处理引号;更严重的是,传输期间目标文件名上已经存在半成品;
  • 自行实现 SFTP v3 只需要 10 个报文,且每一步都能获得明确的成功或失败码,可以在同一条连接内比对字节数并原子改名。

最后一条是关键。picopaste 上传时先写入临时名(.tmp-<ts>-<rand>),经 STAT 校验字节数一致后,才 RENAME 为最终文件名。因此 Claude Code 不可能读到传输未完成的文件。

选择 SFTP 而非自定义协议的原因同样明确:远端零安装这一约束不可突破。

整条上传链路的时序如下,注意「写入剪贴板」与「注入按键」位于全部 SFTP 步骤之后:

SFTP 通道

剪贴板

上传 worker

主线程

SFTP 通道

剪贴板

上传 worker

主线程

#mermaid-svg-DX2wAsebkYsgQuUR{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DX2wAsebkYsgQuUR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DX2wAsebkYsgQuUR .error-icon{fill:#552222;}#mermaid-svg-DX2wAsebkYsgQuUR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DX2wAsebkYsgQuUR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DX2wAsebkYsgQuUR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DX2wAsebkYsgQuUR .marker.cross{stroke:#333333;}#mermaid-svg-DX2wAsebkYsgQuUR svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DX2wAsebkYsgQuUR p{margin:0;}#mermaid-svg-DX2wAsebkYsgQuUR .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DX2wAsebkYsgQuUR text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DX2wAsebkYsgQuUR .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DX2wAsebkYsgQuUR .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DX2wAsebkYsgQuUR .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DX2wAsebkYsgQuUR .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DX2wAsebkYsgQuUR #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DX2wAsebkYsgQuUR .sequenceNumber{fill:white;}#mermaid-svg-DX2wAsebkYsgQuUR #sequencenumber{fill:#333;}#mermaid-svg-DX2wAsebkYsgQuUR #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DX2wAsebkYsgQuUR .messageText{fill:#333;stroke:none;}#mermaid-svg-DX2wAsebkYsgQuUR .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DX2wAsebkYsgQuUR .labelText,#mermaid-svg-DX2wAsebkYsgQuUR .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DX2wAsebkYsgQuUR .loopText,#mermaid-svg-DX2wAsebkYsgQuUR .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DX2wAsebkYsgQuUR .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DX2wAsebkYsgQuUR .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DX2wAsebkYsgQuUR .noteText,#mermaid-svg-DX2wAsebkYsgQuUR .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DX2wAsebkYsgQuUR .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DX2wAsebkYsgQuUR .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DX2wAsebkYsgQuUR .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DX2wAsebkYsgQuUR .actorPopupMenu{position:absolute;}#mermaid-svg-DX2wAsebkYsgQuUR .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DX2wAsebkYsgQuUR .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DX2wAsebkYsgQuUR .actor-man circle,#mermaid-svg-DX2wAsebkYsgQuUR line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DX2wAsebkYsgQuUR :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

连接失败则进入退避重连,由等待超时驱动,无需轮询

PNG 直写临时文件;否则 DIBV5/DIB → WIC 编码(不复制像素)

loop

[每 64 KB]

启动即建连(EnsureChannel),成功即 Ready(托盘变绿)

热键触发,投递上传任务(不阻塞消息循环)

OpenClipboard → 取图 → CloseClipboard

REALPATH "." → MKDIR uploads → OPEN .tmp-<ts>-<rand>

WRITE

CLOSE → STAT(远端字节数与本地比对,不符即中止)

RENAME → clip-<ts>-<rand>.png(此前中断不留半成品)

写入路径文本(CF_UNICODETEXT + OleFlush)

焦点守卫二次校验

SendInput Ctrl+Shift+V(核对事件数)

赞(0)
未经允许不得转载:171主机测评 » 让 Linux 上的 Claude Code/codex 拿到 Windows 截图的小工具
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址