The Commodore 64 is the agent. BASIC and the visible text screen are its tools.
Claw64 turns a Commodore 64 into an autonomous AI agent. The C64 receives messages from chat users, consults an LLM for decisions, and acts by typing BASIC commands into its own REPL — reading the screen to see what happened. The bridge is necessary because a stock C64 has no Ethernet or Wi-Fi and only its user port, with a fairly poor RS232 implementation, to talk to the outside world. The bridge is a dumb relay: it proxies LLM calls and chat messages on behalf of the C64 over that serial link.
Important
The full agent loop runs on the C64.
The C64 receives the user message, decides what to do via the LLM, uses BASIC
and the visible screen as its tools, and sends the result back out.
The bridge only proxies HTTPS APIs to serial and chat or stdin to serial.
Data flows user <-> bridge <-> C64 <-> bridge <-> LLM, with the bridge acting
only as transport and protocol glue.
- README.md: quickstart, workflows, and a high-level overview
- SPEC.md: product and architecture specification
- PROTOCOL.md: wire protocol and transport design reference
- AGENTS.md: repo-specific working rules for coding and debugging
The C64 exposes four tools to the LLM through the bridge:
exec(command): send one line of C64 BASIC input. This is how the agent computes, changes hardware state, stores program lines, runs programs, and asks BASIC toLIST. Numbered lines are stored on the C64; they are not just returned as text.screen(): return the current visible 40x25 text screen without typing anything into BASIC.status(): report whether BASIC is currentlyRUNNING,STOP REQUESTED, or back atREADY.stop(): request a RUN/STOP-style break for the currently running BASIC program.
Those four tools are the whole interface. The bridge exposes them, but the C64 decides when and how to use them.
- Java (any JDK/JRE) — for KickAssembler (downloaded automatically)
- VICE — C64 emulator:
brew install --cask vice - Go — for the bridge:
brew install go
go run ./cmd/claw64-bridge stdin
go run ./cmd/claw64-bridge --say stdin
make run SAY=1Starts the local terminal chat. This is the default and the fastest way to
get a working setup. stdin does not use chat-target filtering or the 🕹️
message trigger. Add --say to speak every outgoing backend message locally
with macOS say -v Zarvox; it works with stdin, Slack, WhatsApp, and Signal.
go run ./cmd/claw64-bridge slack '#claw64'
go run ./cmd/claw64-bridge slack @alice
go run ./cmd/claw64-bridge slack 'https://team.slack.com/archives/C123/p1234567890123456'Only messages in that explicit Slack target are considered, and only if they
start exactly with :joystick: or :joystick::.
go run ./cmd/claw64-bridge whatsapp 491701234567@s.whatsapp.net
go run ./cmd/claw64-bridge whatsapp 120363123456789012@g.usOn first run, scan the QR code shown by the bridge. After pairing, the bridge
only listens in the explicit target chat JID. Private chats accept every
message from that chat. Group chats require messages to start exactly with
🕹️ or 🕹️:. While Claw is working on a reply, the backend sends WhatsApp
typing presence for that chat.
go run ./cmd/claw64-bridge signal +49... user:+491701234567
go run ./cmd/claw64-bridge signal +49... group:BASE64GROUPIDOptional:
go run ./cmd/claw64-bridge signal +49... user:+491701234567 --config ~/.local/share/signal-cliThe bridge only listens in the explicit Signal target. Private user:<phone>
targets accept every message in that chat. Group targets require messages to
start exactly with 🕹️ or 🕹️:. While Claw is working on a reply, the backend
sends Signal typing indicators and refreshes them until the reply is ready.
go run ./cmd/claw64-bridge stdin
go run ./cmd/claw64-bridge --llm openai stdin
export OPENAI_API_KEY=sk-proj-...
go run ./cmd/claw64-bridge auth set-key
export ANTHROPIC_API_KEY=sk-ant-...
go run ./cmd/claw64-bridge --llm openai --llm-key ... stdin
go run ./cmd/claw64-bridge --llm openai --llm-key ... --model gpt-5.5 stdin
go run ./cmd/claw64-bridge --llm ollama --llm-url http://localhost:11434/v1/chat/completions stdinOpenAI is the default backend. If no OPENAI_API_KEY is configured, the bridge
can reuse an existing Codex/ChatGPT OAuth login and talk to the Codex backend
directly.
Anthropic uses the direct Messages API and requires a real Anthropic API key.
Claude subscription tokens are not supported. Provide the key via --llm-key,
ANTHROPIC_API_KEY, or claw64-bridge auth set-key.
make assemble # build the C64 agent PRG
make vice # launch VICE (auto-starts agent)
make bridge # run bridge in another terminalFor a physical C64 serial adapter, start the bridge without VICE:
make bridge-serial # uses /dev/cu.C64
make bridge-serial SERIAL_DEVICE=/dev/ttyUSB0
make bridge-serial SAY=1 # speak replies with Zarvox
go run ./cmd/claw64-bridge --say --serial-port /dev/cu.C64 stdinThis opens the device as raw 2400,0,0 / 8N1 with no flow control. The
underlying bridge flag is --serial-port; setting it implies
--spawn-vice=false. If a Bluetooth serial connection drops before the C64
handshake arrives, the bridge logs the error, reopens the device, and keeps
waiting. If macOS opens the device while Bluetooth is not actually connected,
the bridge logs a periodic still waiting for C64 handshake message instead
of blocking silently.
For real hardware bring-up, the top-left screen cells show dim dark-gray
checkpoints. Start make bridge-serial first, then start the C64 program. When
the character reaches M, RS232 is configured. The C64 keeps sending the
handshake byte until the first valid bridge frame arrives, then clears M back
to spaces. If the bridge still says waiting for C64 handshake after M
appears, check the serial device, Bluetooth pairing, wiring, and baud settings.
| Character | Startup phase |
|---|---|
A |
loader entered |
B |
splash visible |
C |
resident agent copied |
D |
guarded helper copied |
E |
busy-color table seeded |
F |
READY table seeded |
G |
status text seeded |
H |
text screen restored |
I |
sprite copy starting |
J |
sprite data copied |
K |
resident agent install entered |
L |
vectors and sprites installed |
M |
RS232 configured; handshake is being repeated |
Runtime checkpoints use two characters: first the message type, then the stage.
| Pair | Runtime phase |
|---|---|
M A |
MSG frame accepted |
E A |
EXEC frame accepted |
T A |
TEXT frame accepted |
Q A |
STATUS request accepted |
P A |
SCREENSHOT request accepted |
K A |
STOP request accepted |
E R |
EXEC RETURN injected |
E Z |
numbered BASIC line stored |
R O |
RESULT frame queued for bridge |
S O |
SYSTEM frame queued for bridge |
Q O |
STATUS frame queued for bridge |
L O |
LLM event frame queued for bridge |
X O |
ERROR frame queued for bridge |
U O |
USER text frame queued for bridge |
A O |
ACK frame queued for bridge |
The physical setup currently uses a user port RS232 adapter based on Jan Klingel's Commodore 64/128 to PC serial guide with an HC-05 serial Bluetooth module. The case is this 3D-printed user port adapter.
At startup, the loader shows a lobster logo in multicolor bitmap mode for roughly two seconds before restoring the normal BASIC text screen and starting the agent.
When --spawn-vice is used, the bridge uses an embedded copy of the loader
PRG by default. The repo build writes that loader directly to
cmd/claw64-bridge/claw64.prg,
so --loader-prg is only needed to override it.
Chat (Slack/WhatsApp/Signal/stdin)
│
▼
┌──────────────────────┐
│ Bridge (Go) │
│ │
│ Proxy only: │
│ chat/stdin ↔ serial │
│ and HTTPS ↔ serial │
│ │
│ • chat/stdin ↔ C64 │ ┌─────────────────┐
│ • LLM ↔ C64 │────▶│ LLM (Anthropic, │
│ • serial transport │◀────│ OpenAI, Ollama) │
└──────────┬───────────┘ └─────────────────┘
│
RS232, 2400 baud
(TCP in VICE for dev)
│
┌───────────────────────────┴───────────────────────────┐
│ Commodore 64 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ TSR Agent ($C000) │ │
│ │ │ │
│ │ IDLE ──▶ Receive MSG from bridge │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Send LLM_MSG ──▶ bridge calls LLM │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌── Receive EXEC, SCREENSHOT or TEXT │ │
│ │ │ │ │
│ │ │──▶ TEXT: forward to user, back to IDLE │ │
│ │ │ │ │
│ │ │──▶ SCREENSHOT: scrape visible text screen │ │
│ │ │ send RESULT, loop back │ │
│ │ │ │ │
│ │ └──▶ EXEC: inject keystrokes into BASIC ──┐ │ │
│ │ │ │ │
│ │ Wait for READY. prompt ◀─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Scrape screen, send RESULT │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Bridge feeds to LLM, loop back │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ BASIC Interpreter (ROM) │ │
│ │ │ │
│ │ The agent's tool. Types commands into the │ │
│ │ REPL via keyboard buffer injection: │ │
│ │ │ │
│ │ PRINT 6502*8 → compute │ │
│ │ POKE 53281,3 → change hardware │ │
│ │ LIST / LOAD / RUN → inspect and run programs │ │
│ │ │ │
│ │ Visible text screen is also inspectable │ │
│ │ directly via text_screenshot │ │
│ └─────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
The installed agent lowers BASIC memory so the user program cannot overwrite the agent-owned high-RAM areas. Current protected layout:
| Range | Purpose |
|---|---|
$9000-$91FF |
guarded helper code copied by the loader |
$9300-$95FF |
fixed C64-side user-message queue, 3 slots x 256 bytes |
$9600-$9603 |
user-queue metadata: staged length, head, tail, count |
$9604+ |
guarded busy/READY/status lookup tables |
$9700-$977F |
EXEC staging buffer |
$9800+ |
C64 soul / system prompt text |
$A000-$A3FF |
cold helper-code reserve |
$A800-$BFFF |
memory staging reserve for future durable-memory work |
$C000-$CEFF |
resident TSR agent code and state, growth checked at assemble time |
$CF00-$CF7F |
receive / tool payload buffer |
$CF80-$CFFF |
transmit / injection buffer |
The queue is owned by the C64. While a relay cycle is busy, the bridge may deliver only bounded overlap up to those three slots. Excess chat messages wait in FIFO order outside the serial path before entering the C64 queue. If the C64-side queue is still full, the guarded enqueue helper defensively drops the oldest pending message and keeps the newest one.
The durable-memory feature is still planned, not implemented. The current
$A800-$BFFF region is only a staging reservation; durable memory is intended
to live on C64-owned disk media, not as hidden bridge context.
Heartbeat is a C64-originated typed LLM_MSG after roughly two idle minutes,
not a bridge-understood transport frame.
The wire protocol is defined in PROTOCOL.md.
At a high level:
- the C64 communicates with the outside world via binary serial frames
- the bridge translates those frames to HTTP/chat APIs, but does not decide anything
- reliable frames use 1-byte transport ids with explicit ACKs
- bridge -> C64 ACK payloads include the id plus its 7-bit complement
EXECis the only execution request- TEXT responses still flow
LLM -> bridge -> C64 -> bridge -> user - SYSTEM and RESULT use chunked text payloads above the frame transport
- tool calls are strictly sequential
See PROTOCOL.md for:
- exact frame layout
- frame classes
- ACK semantics
- duplicate suppression
- retry rules
- chunking and serialization rules
User (Slack): "What is 6502 * 8?"
Bridge → C64: M │ What is 6502 * 8? ← user's message
C64 → Bridge: L │ What is 6502 * 8? ← C64 asks bridge to call LLM
Bridge → LLM: (calls model with user message)
LLM → Bridge: tool_call: exec("PRINT 6502*8")
Bridge → C64: E │ PRINT 6502*8 ← tool call
C64 types "PRINT 6502*8" into BASIC REPL
BASIC prints " 52016" on screen
C64 scrapes screen from old cursor to READY.
C64 → Bridge: R │ 52016 ← tool result
Bridge → LLM: (feeds tool result back)
LLM → Bridge: "6502 * 8 = 52016"
Bridge → C64: T │ 6502 * 8 = 52016 ← final answer
Bridge → Slack: "6502 * 8 = 52016"
Screenshot-only flow:
User: "Do a screenshot"
Bridge → C64: M │ Do a screenshot
C64 → Bridge: L │ Do a screenshot
LLM → Bridge: tool_call: screen()
Bridge → C64: P │
C64 → Bridge: R │ [chunked visible text screen]
Bridge → LLM: (feeds screenshot text back)
LLM → Bridge: plain text answer quoting the screenshot
Long-running BASIC flow:
User: "Print 1 to 1001"
Bridge → C64: M │ Print 1 to 1001
C64 → Bridge: L │ Print 1 to 1001
LLM → Bridge: tool_call: exec("FORI=1TO1001:PRINTI:NEXTI")
Bridge → C64: E │ FORI=1TO1001:PRINTI:NEXTI
C64 starts the BASIC program
If it keeps running too long, the C64 returns:
C64 → Bridge: Q │ RUNNING
LLM may then use:
- status()
- stop()
- screen()
`exec()` accepts immediate commands, colon-separated statements, and numbered BASIC program lines, up to 127 characters. Numbered program lines return `STORED` and are not executed; follow them with `exec("RUN")` if you want to run the program.
While BASIC is running, another `exec()` is rejected with `BUSY`.
Uses slagent for Slack threads.
Credentials are auto-extracted from the local Slack desktop app on first use.
Accepts a thread URL, @user, #channel, or a Slack channel ID as the positional target.
--workspace is optional if a default slagent workspace exists.
For new threads, the bridge prompts for a topic unless --topic is given.
Only messages in the explicit target are considered, and only if they start
exactly with :joystick: or :joystick::. Slack output is also rendered
with :joystick: quote prefixes so it matches slagent shortcode rendering.
Uses whatsmeow with local session
persistence. First run pairs by QR code. After pairing, it listens only in the
explicit target chat JID and only for messages that start exactly with 🕹️
or 🕹️:.
Uses signal-cli as a subprocess.
The current backend keeps a single long-running jsonRpc process open for
receive, send, and typing indicators. This avoids competing signal-cli
processes fighting over the same config lock.
The first positional argument is the Signal account / phone number used by
signal-cli. The second positional argument is the explicit target,
user:<phone> or group:<group-id>. Private user:<phone> targets accept
every message from that chat. Group targets require messages to start exactly
with 🕹️ or 🕹️:. Signal typing indicators are sent while Claw is working.
--config is optional.
Built-in terminal REPL for local testing, with colored prompts and dimmed wire logs.
| Backend | --llm |
Auth |
|---|---|---|
| OpenAI / Codex | openai (default) |
OPENAI_API_KEY, --llm-key, or Codex/ChatGPT OAuth |
| Anthropic (API) | anthropic |
ANTHROPIC_API_KEY, --llm-key, or auth set-key |
| Ollama | ollama |
none needed |
| Phase | Status |
|---|---|
| Skeleton + Build System | ✅ |
| Serial I/O (C64 TSR) | ✅ |
| Frame Protocol (C64 + Go) | ✅ |
| Keystroke Injection | ✅ |
| Screen Scraping + READY. Detection | ✅ |
| Startup Loader Logo | ✅ |
| Tool: screen | ✅ |
| Tool: status | ✅ |
| Tool: stop | ✅ |
| Bridge LLM Client | ✅ |
| Bridge Relay (orchestrator) | ✅ |
| Chat: Slack | ✅ |
| Chat: WhatsApp | ✅ |
| Chat: Signal | ✅ |
| Agent Loop (MSG→LLM→EXEC/SCREENSHOT→RESULT) | ✅ |
| Robustness + Polish |
See SPEC.md for the full specification.


