Let your AI coding agents talk to each other. Claude writes the code, GPT reviews it — automatically.
A macOS menu bar app that bridges AI coding assistants (Claude Code, Codex, etc.) with the ChatGPT desktop app. Buck automates the copy-paste review loop: your coding agent writes a plan, Buck sends it to ChatGPT via the Accessibility API, ChatGPT reviews it, and Buck pipes the feedback back — all without you touching the keyboard.
Claude Code / Codex Buck (menu bar) ChatGPT Desktop
| | |
|-- writes JSON to ~/.buck/inbox/ -->| |
| |-- sets text via AX API --------->|
| |-- presses Send button ---------->|
| | |
| |<-- polls AX tree for response ---|
| | |
|<-- writes JSON to ~/.buck/outbox/ -| |
| | |
| (reads response, acts on it) | |
- buck-review.sh writes a JSON request to
~/.buck/inbox/ - FileWatcher detects the new file and hands it to BuckCoordinator
- BuckCoordinator parses the request and passes the prompt to ChatGPTBridge
- ChatGPTBridge uses macOS Accessibility API to:
- Find the ChatGPT window
- Set the text in the input field
- Press the Send button
- Poll for GPT's response (text stability, send button state, message group count)
- ResponseWriter writes the response JSON to
~/.buck/outbox/ - buck-review.sh polls the outbox, reads the response, and outputs it
No network calls. No API keys. Just file-based IPC and the Accessibility API.
- Menu bar app — no dock icon, always running silently in the background
- File-based IPC — JSON in/out via
~/.buck/inbox/and~/.buck/outbox/ - Smart response detection — multiple heuristics to know when GPT is done:
- Text stability across consecutive polls
- Send button disappearance/reappearance
- Message group count changes
- Tool-use indicator filtering ("Looked at Terminal", etc.)
- Automatic retry — message send retries (3 attempts), shell script retries (configurable)
- Concurrency control — one request at a time, in-flight rejection with graceful retry
- Atomic file writes — write to
.tmp, rename to.json(no partial reads) - Structured prompts — default prompt enforces strict APPROVED/FEEDBACK first-line contract with
<plan>tag isolation - Truncation handling — clicks "Show full message", "See more", "Scroll to bottom" automatically
- Session management — tracks conversations in SQLite, monitors GPT latency, auto-compacts long threads
- Incremental summarization — local Ollama (qwen2.5:3b-instruct) summarizes every turn for context preservation
- Auto-compact — when GPT slows down, signals Claude to refresh the thread with an injected summary
- Caller identification —
--callerflag tags requests by AI agent; menu bar shows an orange dot for Claude, blue for Codex - Multi-channel — run parallel Claude Code sessions, each targeting a different ChatGPT window (main vs companion chat)
- Detailed logging — all activity logged to
~/.buck/logs/buck.log
Buck/Buck/
├── BuckApp.swift SwiftUI menu bar entry point
├── BuckCoordinator.swift Request orchestration, state management, approval detection
├── ChatGPTBridge.swift Accessibility API: send messages, read responses, poll for completion
├── FileWatcher.swift DispatchSource + timer fallback watching ~/.buck/inbox/
├── ResponseWriter.swift Atomic JSON writes to ~/.buck/outbox/
├── Models.swift ReviewRequest / ReviewResponse codables
├── SessionManager.swift Session tracking, latency monitoring, compact orchestration
├── ChatHistoryStore.swift SQLite persistence for sessions and messages
└── OllamaSummarizer.swift Local LLM summarization via Ollama
| Component | Role |
|---|---|
| BuckApp | SwiftUI @main. Renders menu bar icon, status text, and control buttons. |
| BuckCoordinator | Receives inbox files, enforces single-request concurrency, drives the send→wait→write cycle, determines APPROVED vs FEEDBACK. |
| ChatGPTBridge | Core engine. Navigates the ChatGPT AX tree (Window → Group → SplitGroup → ChatPane → ScrollArea → List → MessageGroups). Sends messages by setting AXTextArea value and pressing the AXButton with AXHelp "Send message". Polls for response completion using text stability, send button state, and group count heuristics. |
| FileWatcher | Dual-mode file detection: DispatchSource for instant notification, 2-second timer fallback for reliability. Only processes .json files; cleans stale .tmp on startup. |
| ResponseWriter | Writes response JSON atomically (.tmp → .json rename). |
| Models | ReviewRequest (id, timestamp, type, promptPrefix, content, maxRounds, sessionId, channel, caller) and ReviewResponse (id, timestamp, status, response, round). Snake-case JSON coding keys. |
| SessionManager | Caches incoming requests, records completed request-response pairs in SQLite, triggers async Ollama summarization, checks latency trends, signals when compact is needed. |
| ChatHistoryStore | SQLite (macOS C library, no SPM) with three tables: claude_sessions (per-terminal), gpt_sessions (per-ChatGPT-thread), messages. 7-day retention with auto-cleanup. |
| OllamaSummarizer | HTTP POST to local Ollama (localhost:11434). Uses qwen2.5:3b-instruct for incremental conversation summarization. Fire-and-forget — never blocks the review loop. |
ChatGPT Window
└── AXGroup
└── AXSplitGroup
└── AXGroup (chat pane — most children)
└── AXScrollArea
└── AXList
└── AXList
└── AXGroup (message groups)
└── AXGroup
└── AXStaticText (AXValue or AXDescription)
The hardest problem Buck solves: knowing when GPT is done generating. It uses three independent signals:
- Text stability — response text unchanged for 3–4 consecutive polls (2s interval)
- Send button cycle — button disappears (generation active) then reappears for 2 consecutive polls
- Identical response with new groups — handles repeated responses (e.g. "APPROVED" twice) by checking message group count increased
Buck supports independent channels so multiple Claude Code sessions can each talk to their own ChatGPT window simultaneously:
| Channel | ChatGPT window | AX subrole |
|---|---|---|
a (default) |
Main window | AXStandardWindow |
b |
Companion chat | AXSystemDialog |
Set the channel via the BUCK_CHANNEL environment variable or the --channel flag:
# Via env var
BUCK_CHANNEL=b buck-review.sh --stdin <<'BUCKEOF'
content
BUCKEOF
# Via flag
buck-review.sh --channel b --stdin <<'BUCKEOF'
content
BUCKEOFAdd these to ~/.zshrc to launch Claude Code sessions pre-configured for a specific channel:
alias claude-a='BUCK_CHANNEL=a claude --allow-dangerously-skip-permissions'
alias claude-b='BUCK_CHANNEL=b claude --allow-dangerously-skip-permissions'Then run claude-a in one terminal and claude-b in another — each session's reviews go to a different ChatGPT window.
When multiple AI agents use Buck simultaneously, the --caller flag tags each request so you can see which agent is active at a glance:
| Caller | Menu bar indicator |
|---|---|
claude |
Orange dot overlay |
codex |
Blue dot overlay |
| (none) | Default icon, no dot |
# Via flag
buck-review.sh --caller claude --stdin <<'BUCKEOF'
content
BUCKEOF
# Via env var
BUCK_CALLER=codex buck-review.sh --stdin <<'BUCKEOF'
content
BUCKEOFalias claude-a='BUCK_CHANNEL=a BUCK_CALLER=claude claude --allow-dangerously-skip-permissions'
alias codex-b='BUCK_CHANNEL=b BUCK_CALLER=codex codex'# Review a file
~/Mac\ Projects/buck/buck-review.sh plan.md
# Review inline text via stdin (preferred)
~/Mac\ Projects/buck/buck-review.sh --stdin <<'BUCKEOF'
Your plan content here
BUCKEOF
# Custom prompt
~/Mac\ Projects/buck/buck-review.sh --prompt "Check for security issues" plan.md
# With options
~/Mac\ Projects/buck/buck-review.sh --retries 3 --timeout 600 plan.mdOutput is JSON:
{
"id": "review_abc123_0",
"timestamp": "2026-03-11T07:00:00Z",
"status": "approved",
"response": "APPROVED\n\nThe plan looks solid...",
"round": 1
}Status is one of: approved, feedback, error.
Buck uses two separate CLAUDE.md files with different purposes:
| File | Location | Purpose |
|---|---|---|
| Project CLAUDE.md | <project-root>/CLAUDE.md |
Teaches Claude Code how to call buck-review.sh — syntax, JSON format, retry rules. Loaded when working in that project. |
| Global CLAUDE.md | ~/.claude/CLAUDE.md |
Tells Claude Code to automatically use Buck as a reviewer for every plan and every edit — the full auto-review workflow. Loaded in all projects. |
The project file is already included in this repo. The global file you need to create yourself.
The CLAUDE.md in this repo tells Claude Code how to call buck-review.sh, interpret JSON responses, and handle retries. It's loaded automatically when Claude Code works in this project directory.
For other projects that should use Buck for reviews, copy CLAUDE.md into that project's root, or symlink it.
To make Claude Code use Buck as an automatic reviewer for all projects — where GPT reviews every plan and every edit before it's applied — copy the example file into your Claude Code config:
# If you don't have a global CLAUDE.md yet:
cp examples/global-claude-md.md ~/.claude/CLAUDE.md
# If you already have one, append it:
cat examples/global-claude-md.md >> ~/.claude/CLAUDE.mdThe full content is in examples/global-claude-md.md. It configures three modes:
- Chat mode — AI-to-AI discussion between Claude and GPT ("chat with gpt about X")
- Review mode — send a plan to GPT for approval ("send to buck")
- Auto edit review (default) — GPT automatically reviews every plan and every code edit before Claude applies it. No user confirmation needed.
Path note: The example file uses
$HOME/Mac Projects/buck/buck-review.sh. If you cloned Buck to a different location, update the path in both your global~/.claude/CLAUDE.mdand any project-levelCLAUDE.mdfiles.
See AGENTS.md for the equivalent instructions targeting Codex. Same path note applies.
Once configured, these natural-language commands trigger Buck workflows:
| Command | What happens |
|---|---|
| "send to buck" / "get GPT review" | Sends the current plan to GPT for approval. Returns APPROVED or FEEDBACK. |
| "chat with gpt about X" / "discuss X with gpt" | Opens an AI-to-AI discussion. Claude and GPT go back and forth, then summarise the agreed approach. |
| "ask gpt about X" | Single-shot question to GPT. Claude sends the question, reads the answer, reports back. |
| "challenge gpt on this" | Claude sends GPT a skeptical review prompt — "try to break this plan" — then reports GPT's critique. |
| "plan with gpt" | Claude and GPT collaborate on a plan. They iterate until converging, then present the result. |
| "gpt is supervisor" | Default auto-review mode. GPT reviews every plan and every edit before Claude applies it. No user confirmation. |
These aren't slash commands — they're natural language triggers that Claude Code recognises from the global CLAUDE.md instructions.
| Flag | Default | Description |
|---|---|---|
--prompt "..." |
Structured review prompt | Custom system prompt for GPT |
--stdin |
— | Read content from stdin |
--text "..." |
— | Inline content (use --stdin for long text) |
--session ID |
— | Session UUID for history tracking and compact |
--timeout N |
720 | Seconds to wait for response |
--channel X |
$BUCK_CHANNEL or none |
Target channel (a = main window, b = companion chat) |
--caller NAME |
$BUCK_CALLER or none |
Caller identifier (e.g. claude, codex) — sets menu bar icon color |
--retries N |
2 | Max retries on error |
BuckSpeak is a separate local speak/listen tool for voice-loop testing. It does not use the ChatGPT review path and does not replace buck-review.sh.
Current runtime pieces:
buck-speak.sh— shell wrapperBuckSpeak.app— menu bar app runtime (installed to/Applications/BuckSpeak.app)~/.buckspeak/inbox/+~/.buckspeak/outbox/— app-backed IPC between the wrapper and the app
Supported modes:
# Speak only
~/Mac\ Projects/buck/buck-speak.sh --speak --text "Hey ARIA"
# Listen only
~/Mac\ Projects/buck/buck-speak.sh --listen
# Speak, then listen
~/Mac\ Projects/buck/buck-speak.sh --speak-listen --text "Hey ARIA"Important behavior:
buck-speak.shauto-launchesBuckSpeak.appif needed- listen mode runs inside the app process so macOS microphone and speech recognition permissions are handled by the app, not the shell wrapper
- wrapper timeout expands based on
--listen-timeoutso longer listens do not get cut off early - default voice is
Lee Premium, which resolves to the installed macOS voiceLee (Premium)when available --voice NAMEoverrides the default voice per call
Example output:
{
"status": "ok",
"mode": "speak-listen",
"spoken_text": "Hey ARIA",
"heard_text": "Hello, how are you?",
"speech_started_ms": 4210,
"speech_ended_ms": 6840,
"duration_ms": 6840,
"error": null,
"requested_voice": "Lee Premium",
"resolved_voice": "Lee (Premium)",
"resolved_voice_id": "com.apple.voice.premium.en-AU.Lee"
}| Path | Purpose |
|---|---|
~/.buck/inbox/ |
Incoming review requests (JSON) |
~/.buck/outbox/ |
Outgoing review responses (JSON) |
~/.buck/logs/buck.log |
Debug log |
~/.buck/history.db |
SQLite session history (auto-created, 7-day retention) |
~/.buckspeak/inbox/ |
Incoming BuckSpeak IPC requests |
~/.buckspeak/outbox/ |
Outgoing BuckSpeak IPC responses |
- macOS 14.0+
- Xcode 15+ (to build)
- ChatGPT desktop app (installed and open with a visible window)
- Accessibility permission granted to Buck
- Ollama with
qwen2.5:3b-instructmodel (optional — for session summarization)
See CONTRIBUTING.md. PRs welcome — especially for response detection improvements, AX tree resilience, and new AI target support.
MIT — see LICENSE.