Never write a daemon for a Claude Code tool again. cc-interact extracts cc-review's daemon, event log, SSE, edit gate, and MCP plumbing into a reusable Go framework; the echo example is 381 lines.
go get github.com/yasyf/cc-interactThat run is examples/echo, end to end. Two domain handlers, one REST mount, and one MCP tool exercise the whole framework. A human POSTs an item, the agent replies through its channel, and both events come back off the same /events plane a browser would read.
Driving with an agent? Paste this:
Run `go get github.com/yasyf/cc-interact` in my Go project.
Study https://github.com/yasyf/cc-interact/tree/main/examples/echo — a complete headless consumer — then stand up a human-in-the-loop surface for my domain: start/reply handlers, a REST mount, and one MCP channel tool.
Verify the round trip: POST an item to the daemon's REST plane and confirm the agent's reply streams back over /events.
A review tool that can't stop the agent from editing mid-review is a suggestion box. Inject the verdict as one function:
daemon.Config{
// cc-review: block while a review is open.
Gate: func(ctx context.Context, s subject.Subject, tool daemon.ToolCall) (bool, string) {
return s.Status != "open", "a human is still reviewing — reply to their comments first"
},
GateErrorReason: "review state unreadable; blocking edits",
}The guard-edit hook routes every edit the Claude session attempts through this verdict. While the subject is open, Claude sees your reason instead of a completed write. Errors reading the subject fail closed (GateErrorReason), and a missing daemon fails open, so a crashed daemon never bricks the session.
A socket for the UI plus polling for the agent gives you two realtime paths that drift, drop events, and disagree after a reconnect. Here both roles read one log:
mkdir -p "$HOME/.cc-echo/bin"
go build -o "$HOME/.cc-echo/bin/echo" ./examples/echo
"$HOME/.cc-echo/bin/echo" watchwatch streams the same append-only log as the browser's /events, one JSON event per line, with exclude_origin=agent so the agent never reacts to its own echo. Delivery is at-least-once with a persisted per-consumer cursor, so a dropped connection resumes where it left off, and the consumer survives a daemon swap.
The binary is half the ship surface; the MCP server wiring, session hooks, and start skill that make up the plugin payload are their own pile of boilerplate. Render it instead:
./plugin-template/render.sh out/ PLUGIN_NAME=mytool DISPLAY_NAME=MyTool \
BINARY_NAME=mytool RELEASE_REPO=you/mytool \
MCP_SUBCOMMAND=channel SKILL_NAME=mytool:startYou get cc-review's plugin payload with the review strings swapped for yours. The rendered tree carries the channel MCP server wiring, a capt-hook hook pack (session record, edit guard, and the optional agent-steering plane — dispatched by the captain-hook plugin the manifest declares as a dependency), and a start skill; the release-binary installer arrives separately, as a cc-guides fragment layout in your plugin's repo. plugin-template/ documents every variable, the pack contract, and the installer layout.
One process model, shipped. A launchd-managed daemon owns a single-writer SQLite (WAL) holding an append-only per-subject event log; the browser, watch, and the MCP channel all read the same SSE plane. Daemonkit owns exact service convergence, signed peer roles, readiness, and receipt-authenticated replacement. You register domain ops against a generic control envelope and the framework routes the rest.
| Package | Owns |
|---|---|
daemon |
exact service convergence, signed roles, durable stop, the op registry, and the edit gate |
store |
exact v1 pure-Go SQLite holding subjects, the event log, and declarative consumer tables |
event |
the log-entry type and the per-subject pub/sub wakeup |
sse |
the HTTP /events plane, at-least-once SSE with Last-Event-ID resume |
consume |
the agent-side SSE client with a persisted resume cursor |
channel |
the stdio MCP server, tools in and notifications out |
cmd |
ready-made daemon, watch, status, and stop cobra constructors plus the hidden hook and channel entry points |
subject |
ownership, one subject per window and scope, stable across /clear and compaction |
paths |
the ~/.<app> state layout of socket, DB, HTTP handshake, and locks |
vcs |
optional git/jj working-copy snapshots and the per-prompt turn ledger |
A headless consumer never imports it. When you want a diff-style web client:
npm install @cc-interact/reactMount sse.StaticHandler on the daemon and the React package's stream and query primitives read the same /events plane as everything else.
http.json and daemon.log live directly under ~/.<app>/. The daemon's own
runtime files — the socket, the owner record, the start lock — live under
~/<Daemon.Label>/, which daemonkit derives from the label alone so the
launcher and the daemon cannot disagree about where to find each other.
The derived database and consumer cursors live under the exact
~/.<app>/cc-interact-v1/ namespace. A consumer supplies declarative
StoreSchema DDL; the store creates it once and thereafter requires both
user_version = 1 and the exact core-plus-consumer fingerprint. There are no
migrations, compatibility readers, or in-place schema changes.
The API is pre-1.0 and moves with cc-review; every break lands in the changelog. Licensed under PolyForm Noncommercial 1.0.0.

