Codex Buddy Bridge connects Codex Desktop to a small ESP32 hardware buddy. It keeps the device updated with the current Codex work state, such as idle, busy, waiting for user input, approval prompts, recent activity, and token counters.
The project is made of two local pieces:
- a Codex MCP plugin that exposes
buddy_*tools and writes the latest state - a Python BLE bridge that sends that state to compatible ESP32 hardware
The hardware side follows the Claude Desktop Buddy Nordic UART protocol, so a device or firmware that works with Claude Desktop Buddy can be used for testing this bridge.
Start here: QUICKSTART.md
中文文档:README.zh-CN.md
flowchart LR
subgraph Codex["Codex Desktop"]
S1["Session A"]
S2["Session B"]
MCP["MCP tools"]
Hooks["Plugin hooks"]
end
subgraph Files["Shared bridge files"]
State["bridge/state.json"]
Activity["bridge/activity.json"]
Command["bridge/command.json"]
Usage["bridge/usage_state.json"]
end
subgraph Bridge["Single persistent bridge.py process"]
Heartbeat["Heartbeat loop"]
Commands["Command loop"]
Tokens["Token tracker"]
BLE["Nordic UART BLE client"]
end
Hardware["Claude Code Buddy compatible ESP32 hardware"]
CodexDB["%USERPROFILE%/.codex/state_5.sqlite"]
S1 --> MCP
S2 --> MCP
MCP --> State
MCP --> Command
Hooks --> Activity
CodexDB --> Tokens
Activity --> Heartbeat
State --> Heartbeat
Command --> Commands
Tokens --> Usage
Tokens --> State
Heartbeat --> BLE
Commands --> BLE
BLE <-->|"NUS: RX write / TX notify"| Hardware
The bridge sends newline-delimited JSON heartbeat snapshots over BLE every 10 seconds. The ESP32 firmware can use those snapshots to show idle, busy, attention, approval prompt, and transcript states.
Use this project when you want a physical desktop indicator for Codex:
- show whether Codex is currently working, idle, blocked, or waiting for you
- surface short task summaries without keeping the Codex window in focus
- forward approval prompts to hardware buttons and read approve/deny responses
- display recent tool activity and token usage while a bridge process runs
It is intentionally local-first. The MCP server updates JSON files in this repo, and the bridge reads those files and pushes compact heartbeat snapshots over BLE. No cloud service is required by this project.
For hardware-side testing, use a device or firmware compatible with
Claude Desktop Buddy.
Codex Buddy Bridge scans for a BLE device whose name starts with Claude by
default and talks to it through the Nordic UART Service.
If you already have Claude Desktop Buddy hardware working, you can usually test this project directly by starting the bridge and checking device status:
buddy_start_bridge
buddy_get_bridge_status
buddy_get_device_status
.codex-plugin/plugin.json Codex plugin manifest
.mcp.json MCP server registration
mcp/server.mjs Node MCP tool server
bridge/bridge.py Python BLE Nordic UART bridge
bridge/config.json Local bridge config
bridge/state.json Current heartbeat snapshot
bridge/usage_state.json Token tracking cursor and counters
bridge/activity.json Latest low-level tool activity from hooks
Install the Node MCP dependency once:
npm installPython dependencies are expected to already be available in the configured
Python environment. This project uses bleak for BLE.
buddy_start_bridge: start the BLE bridge in the background.buddy_stop_bridge: stop the bridge process.buddy_get_bridge_status: read process, BLE, and current snapshot status.buddy_set_snapshot: update the heartbeat snapshot.buddy_task_start: mark the current Codex request as busy.buddy_task_attention: mark Codex as waiting for user attention.buddy_task_done: mark Codex as idle after completing a request.buddy_task_error: mark Codex as needing attention after an error.buddy_set_prompt: show a permission prompt on the buddy.buddy_clear_prompt: remove the current prompt.buddy_get_permission_response: read the latest approval decision from the buddy.buddy_clear: return to idle state.buddy_get_device_status: send{"cmd":"status"}through BLE and wait for the ack.
The bridge is intended to be started manually once and left running. MCP tools
only update bridge/state.json; they do not implicitly start the bridge.
While connected, the bridge runs independent loops for heartbeat, command acks,
and token tracking. Heartbeats are still sent every heartbeatSeconds seconds
(10 by default), and state or hook activity changes are sent immediately.
Token tracking reads Codex's local state database in read-only mode
(%USERPROFILE%\.codex\state_5.sqlite by default). It tracks positive deltas
from the latest active thread's tokens_used value and stores its cursor in
bridge/usage_state.json so bridge restarts do not re-count old tokens.
Token updates are throttled before they are written to bridge/state.json:
by default the bridge flushes at most 5,000 tokens per update, after either
5,000 pending tokens or 60 seconds. This avoids flooding the device with rapid
level-up updates during long coding sessions.
Plugin hooks write low-level tool activity to bridge/activity.json. The bridge
merges that activity into the next heartbeat; hooks never touch BLE and never
start the bridge.
This repo includes AGENTS.md instructions that tell Codex to update the
Buddy state during work in this project:
- user request starts:
buddy_task_start - waiting for confirmation:
buddy_task_attentionorbuddy_set_prompt - request completes:
buddy_task_done - request fails or needs intervention:
buddy_task_error
Short summaries are enabled by default and are sent as the latest entries
items in the heartbeat snapshot. This is project-local behavior; to make it
global, copy the same rules into your global Codex instructions after testing.
When the hardware sends a permission decision such as approve or deny, the
bridge records it in bridge/permission_response.json and clears the matching
prompt from bridge/state.json so the device returns to idle instead of staying
on the approval sending screen.
The bridge scans for the first BLE device whose name starts with Claude and
uses the Nordic UART Service:
- Service:
6e400001-b5a3-f393-e0a9-e50e24dcca9e - RX write:
6e400002-b5a3-f393-e0a9-e50e24dcca9e - TX notify:
6e400003-b5a3-f393-e0a9-e50e24dcca9e
Edit bridge/config.json if your device name prefix or Python path differs.
node --check mcp/server.mjs
python -m py_compile bridge/bridge.py
Get-Content .codex-plugin/plugin.json | ConvertFrom-Json
Get-Content .mcp.json | ConvertFrom-Json
Get-Content hooks.json | ConvertFrom-Json