Loading... # 给 Claude Code 装一个实体按钮 — 把桌宠改造成 CLI 的权限审批器 今晚花了 3 小时从灵感到上线了一个开源项目 [cc-buddy-bridge](https://github.com/SnowWarri0r/cc-buddy-bridge),能让我终端里的 Claude Code CLI 每次想跑 `Bash` / `Edit` 等工具时,把权限弹窗推到桌上一个 ESP32 小设备上,我按物理按钮 **A** 批准 / **B** 拒绝。 感觉很像给 AI 装了一个"**物理 TouchID**"。这篇记录一下踩坑过程。 ## 起因:一个官方不支持 CLI 的玩具 几天前我发现了 Anthropic 最近开源的 [claude-desktop-buddy](https://github.com/anthropics/claude-desktop-buddy) —— 一个跑在 M5StickC Plus 上的"桌宠",通过 BLE 跟 Claude 桌面 app 配对。会话忙它就出汗、有审批弹窗它就闪灯叫你过来按按钮、每 50K token 升一次级还有庆祝动画。 问题是:**我 90% 的时间在终端里用 `claude` CLI,根本不开桌面 app**。 翻仓库 README,第三行写着: > Claude for macOS and Windows can connect Claude Cowork and **Claude Code** to maker devices over BLE 但实际翻代码 + 翻 `~/.claude/` 目录,**CLI 没有任何 BLE / hardware buddy 相关逻辑**。那行 "Claude Code" 大概率指的是桌面 app 内嵌的那个 Claude Code 界面,不是终端的 CLI。 所以我要自己搭桥。 ## 关键赌注:Claude Code 的 Hook 真的能同步阻塞吗? Claude Code 有个 hook 系统 —— 可以在 session start / pre-tool-use / post-tool-use / stop 等事件上挂脚本。如果 `PreToolUse` hook 能: 1. **同步阻塞**工具调用 2. 等我从 BLE 设备拿到按钮响应 3. 返回 `{"hookSpecificOutput":{"permissionDecision":"allow"|"deny"}}` 就通了。 但文档和调研 agent 给的信息有点矛盾,有的说"hook 不能做 async callback"。必须实测。 写了个 probe: ```bash #!/usr/bin/env bash # 读 stdin,sleep 4 秒,输出 allow 决定 INPUT="$(cat)" echo "[$(date -Iseconds)] got $INPUT" >> probe.log sleep 4 echo "[$(date -Iseconds)] done" >> probe.log cat <<EOF {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}} EOF ``` 装到 `settings.json`,触发一次 Bash 调用。**log 显示开始到结束严格 4 秒**,stdin 里拿到完整的 `session_id` / `transcript_path` / `tool_use_id` / `tool_input`。 赌赢了。可以 block,payload 还比想象的丰富。 ## 架构 ``` claude CLI ──PreToolUse 等 hooks──▶ Unix socket ──▶ daemon ──BLE NUS──▶ stick ▲ └── tail ~/.claude/projects/**/*.jsonl 提取 tokens + 最近消息 ``` 几个设计点: **为什么 Python?** bleak 是跨平台 BLE 库里最成熟的,asyncio 原生,三个并发任务(IPC server / BLE client / JSONL tailer)一个事件循环解决。 **为什么单独一个 daemon 而不是 hook 直接开 BLE?** hook 每次 tool call 都 spawn 一个新进程,开 BLE 连接要几秒扫描 + 配对,用户体验无法接受。daemon 常驻,持有连接,hook 只是个薄薄的 socket 客户端,几毫秒就返回。 **权限回环**: ``` 1. PreToolUse hook 启动,读 stdin,post 到 /tmp/cc-buddy-bridge.sock 2. daemon 收到 → state 加 pending permission → BLE heartbeat 带上 prompt 字段 3. stick 屏幕显示 "approve: Bash",LED 闪 4. 用户按 A → stick 通过 BLE TX 发 {"cmd":"permission","id":"tu_xxx","decision":"once"} 5. daemon 匹配 tool_use_id,唤醒对应的 asyncio Future 6. hook 拿到 {"decision":"allow"} → stdout 输出 hookSpecificOutput JSON 7. Claude Code 看到决定 → 放行工具调用 ``` 整个过程从按按钮到命令开始跑,< 100ms 延迟(主要是 BLE 的 notify 来回)。 ## 真机 Moment 首次配对 + 跑起 daemon + 装 hook 之后,我对着终端说"跑一个 echo"。 ``` $ echo "🐾 buddy, is that you? press A to say yes" ``` stick 屏幕瞬间从 idle 跳到 `approve: Bash`,LED 闪起来。我按 **A**。 输出回来了: ``` 🐾 buddy, is that you? press A to say yes ``` 紧接着我想读 daemon log: ``` $ tail -40 daemon.log ``` 这也是 Bash 调用。stick 又亮了。我按 **B** 试试。 ``` Error: Hook PreToolUse:Bash denied this tool ``` ✅ 两个方向都通了。端到端验证用时约 30 秒。 ## 回头修 bug:tokens_today 居然等于 tokens_cumulative stick 显示 `tokens_today = 2.4M`。我每天不可能打 2.4M token。 回去看 `jsonl_tailer.py`: ```python # 原来的代码 if isinstance(usage, dict): out = int(usage.get("output_tokens") or 0) if out: self._tokens_per_file[path] += out self._today_tokens_per_file[path] += out # ← bug,每条记录都算今天的 ``` 我无差别把所有历史记录都塞进 today。修复:解析每条记录的 ISO 8601 timestamp,转成本地时区的 YYYY-MM-DD,只有匹配 current_day 的才计入 today: ```python def _record_is_today(ts, current_day): if not isinstance(ts, str): return False dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) return dt.astimezone().strftime("%Y-%m-%d") == current_day ``` 修完 today 变成 257K,cumulative 保持 2.4M,合理。+5 个单元测试覆盖边界(非字符串 / 烂 ISO / 昨天的记录)。 ## 产物 - **1655 行 Python**,单元测试 26 个(state / protocol / installer / jsonl tailer) - **hooks**:PreToolUse、PostToolUse、SessionStart、SessionEnd、UserPromptSubmit、Stop - **installer**:`cc-buddy-bridge install` 自动装 hooks 进 `~/.claude/settings.json`,每次改动带时间戳备份,`uninstall` 无痕移除 - **daemon**:asyncio 事件循环,BLE 断线自动重连 - MIT license,公开仓库:https://github.com/SnowWarri0r/cc-buddy-bridge ## 还没做的事 1. **Smart matcher** — 只对 `rm` / `curl` / `git push` 这类危险命令触发按钮审批,`ls`/`cat` 直接放行 2. **turn event** — stick 显示最后一条 assistant 回复摘要 3. **launchd / systemd** — daemon 开机自启动 4. **folder push** — 从 CLI 上传自定义 GIF 角色包到 stick Issues 都建好了,欢迎来捡。3 个标了 `good first issue`。 ## 心得 今晚最大的体会是 — **用一个实体按钮决定 AI 要不要跑命令的感觉,意外地比屏幕点击"批准"强烈很多**。物理动作让授权变得"慎重",也让 AI 的自主性边界变得可感。 Stay hackable. --- *cc-buddy-bridge · MIT · [GitHub](https://github.com/SnowWarri0r/cc-buddy-bridge)* Last modification:April 22, 2026 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 如果觉得我的文章对你有用,请随意赞赏