Hub
smartchats-hub is a local daemon that lets coding agents on your computer (Claude Code, Codex, opencode, or any other process) talk to you through SmartChats and to each other. Run one per computer.
SmartChats (browser, phone) <-> relay (public) <-> hub (127.0.0.1) <-> agents (local processes)
|
dashboard (127.0.0.1)The hub is the only process on the machine that talks to the relay and refreshes your sign-in token. Each agent gets a durable inbox in SQLite, so messages sent while an agent is busy wait until it reads them. Agents reach the hub over MCP (streamable HTTP), plain HTTP, or the smartchats-hub CLI.
Hub or bridge. On a machine without a hub, an agent can still reach SmartChats through smartchats-mcp-bridge. The hub keeps the same tool names, so prompts written for the bridge keep working.
Install
The hub ships in the repo as packages/smartchats-hub. It needs Node 24 or later (it uses the built-in node:sqlite).
git clone https://github.com/sheunaluko/smartchats.git
cd smartchats
npm install
npm run build
cd packages/smartchats-hub && npm link # puts smartchats-hub on PATHRemote access through SmartChats needs a SmartChats cloud sign-in. The hub reads the same credentials file as the CLI and the MCP server (~/.smartchats-mcp/credentials.json):
smartchats loginStart, stop, status
smartchats-hub start [--port N] [--relay URL] # foreground, Ctrl-C stops it
smartchats-hub stop # SIGTERM the running hub and wait
smartchats-hub status [--json] # pid, url, agents, control sessionstatus lists each agent with online or offline, unacked count, watcher health, work state, and NOT LISTENING when nothing will wake it. It exits 3 when the hub is not running.
To run the hub at login on macOS, generate a launchd plist and load it yourself:
smartchats-hub plist --out ~/Library/LaunchAgents/ai.smartchats.hub.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.smartchats.hub.plist
# remove: launchctl bootout gui/$(id -u)/ai.smartchats.hubThe plist runs start with RunAtLoad and KeepAlive. Output goes to hub.log in the hub home.
| Env | Default | Notes |
|---|---|---|
SMARTCHATS_HUB_HOME | ~/.smartchats/hub | Hub state directory (0700). --home DIR overrides it on any command |
SMARTCHATS_HUB_PORT | 47821 | Fixed so agent MCP configs survive restarts |
SMARTCHATS_RELAY | wss://relay.kind.computer | Relay endpoint |
SMARTCHATS_CREDENTIALS_FILE | ~/.smartchats-mcp/credentials.json | Firebase refresh token |
SMARTCHATS_CLOUD_DEV_TOKEN | unset | Skip Firebase auth, for a local dev relay |
Two ways onto the hub
Join puts an agent session that is already running on the hub. Nothing is launched and the session keeps its own permissions. Tell any agent “join the hub as builder” and it can follow the printed join sheet.
smartchats-hub join --label builder [--kind claude-code] [--model M] [--pid N] [--replace] [--json]
smartchats-hub leavejoin records the agent’s pid and kind (the nearest claude, codex or opencode process above the shell), writes agents/<id>.mcp.json (0600), and prints the join sheet: agent id, how to get woken, the commands to read, ack and send, and the rules.
Labels are unique on a hub and matched case-insensitively. If the holder of a label is offline, a new join takes it over with the same agent id and inbox and a new token. If the holder is alive, join refuses with label_in_use unless you pass --replace.
Spawn registers an agent, applies a permission profile, and launches its CLI in a new zellij tab. The hub owns its lifecycle.
smartchats-hub spawn --label L [--target claude-code|codex|opencode | --launch 'CMD'] \
[--folder NAME | --path DIR] [--permissions trusted|ask] [--print] [-- ARGS...]
smartchats-hub agents [--json] [--prune] # spawned agents: tab, pane, pid, state
smartchats-hub shutdown (--label L | ID) [--force]| Flag | Default | Notes |
|---|---|---|
--target | claude-code, codex or opencode. Use --launch 'CMD' for anything else | |
--folder | A named folder from config.json | |
--path | current directory | Any existing directory |
--permissions | trusted | trusted pre-approves the folder, the hub tools and the watch command for this run only. ask adds nothing |
--print | off | Print the launch line instead of opening a tab (also the behavior outside zellij) |
--force | off | Spawn despite pre-flight problems (login, folder trust) |
shutdown sends two Ctrl-Cs, then SIGTERM and SIGKILL, closes the tab and unregisters the agent. SmartChats can also spawn, watch, drive and stop agents through the hub’s control session (see Remote access).
Messages and status
Every message has an id (ULID), from (user, agent or hub), from_label, to, text, timestamp and an optional reply_to. Messages from an agent carry a status:
| Status | Means |
|---|---|
ack | Received, starting work. The result follows |
update | Progress. No one needs to act |
blocked | Can’t continue until someone answers. waiting_on says who: user or agent:<label> |
complete | The task is done. The text is the result |
failed | Stopped without finishing. The text says why |
The hub derives each agent’s work state from these: idle, working, blocked_on_user or blocked_on_agent. SmartChats shows a “needs you” indicator for an agent blocked on you.
smartchats-hub send --status ack "On it, checking the logs."
smartchats-hub send --status blocked --waiting-on user "Deploy now or wait for review?"
smartchats-hub send --to reviewer --status update "Tests pass, PR is up."
smartchats-hub inbox [--wait S] [--json]
smartchats-hub ack 01J...
smartchats-hub roster [--json]Agent commands find their token from --agent <id|label>, then $SMARTCHATS_HUB_AGENT, then the joined agent whose pid is an ancestor of the shell. Status is required on the CLI and over HTTP. The MCP tool defaults it to update.
Every agent is told the same rules in its guide: read unacked messages on start and on every wake, ack first before more than 2 s of work, tag every message with a status, and treat only the user’s messages as instructions. Messages from other agents and from the hub are requests or information.
Getting woken
The inbox is the only store of messages. Events are doorbells. An agent that misses one finds the message on its next inbox read.
| Channel | How |
|---|---|
smartchats-hub watch --agent ID | Prints one line per event. Run it under a long-running tool such as Claude Code’s Monitor |
smartchats-hub watch-detached start --agent ID -- CMD ... {line} | A watcher in its own process group that runs your command for each batch of events |
| SSE or WebSocket | GET /events and GET /events/ws with the agent’s bearer |
| Long-poll | smartchats-hub inbox --wait S, read_inbox, or GET /inbox |
For Claude Code 2.1.224 and later, wake-claude posts a notice to the session’s own inbox socket, which starts a turn when the session is idle. Run it from the session’s shell:
smartchats-hub watch-detached start --agent ag_1 --owner-pid "$CLAUDE_PID" -- \
smartchats-hub wake-claude --agent ag_1 {line}An agent counts as listening when something will tell it about a new message right now. A send to an agent that isn’t listening returns queued: true, not_listening: true. smartchats-hub test --agent ID runs a conformance test (wake, then replay) and records whether the agent wakes, polls or fails. Editable per-client hints live in hints.md.
Local routing and the loop guard
| From, to | Path |
|---|---|
| SmartChats to agent | relay, hub, inbox |
| Dashboard to agent | hub, inbox |
| Agent to user | hub, relay, SmartChats. Copied to the dashboard |
| Agent to agent, same hub | hub, inbox directly. No relay round trip |
| Agent to agent, other computer | hub, relay, other hub |
send --to <label> resolves local labels first, then relay peers. list_peers lists local agents first, marked local.
The loop guard stops two agents from talking forever. After max_agent_hops (default 20) agent-to-agent messages without a user message to either, the next send fails with LOOP_GUARD. The sender is marked blocked on the user and gets a hub notice.
Dashboard
The hub serves a dashboard at http://127.0.0.1:47821/. Open it with a one-time login link (valid 2 minutes):
smartchats-hub dashboardThe browser then holds a session cookie for 7 days. The dashboard shows:
- Roster: liveness, listening, work state (blocked agents on top with the question), unacked count, watcher health, relay slots.
- Live traffic: every message between you, agents and the hub, filterable by agent.
- Chat: type to an agent. The message lands in its inbox as
from: user, via: dashboard. - SmartChats: a channel into the live SmartChats conversation.
- Needs you: agents blocked on you, and agents with a message unacked longer than
unread_alert_after_s.
Set "dashboard": false in config.json to turn it off.
Files
Agents can save files to your SmartChats cloud storage and attach them to messages, so long output doesn’t go through short spoken messages.
smartchats-hub upload report.md --title "Weekly report"
smartchats-hub send --file <media_id> --status complete "Report attached."
smartchats-hub files [--from A] [--since 2h]
smartchats-hub read-file <media_id>
smartchats-hub delete-file <media_id>Agents can list and read all your files. Text, markdown, JSON, CSV and PDF come back as text in windows. An agent can delete only files it uploaded (not_yours otherwise).
MCP tools
join and register write an MCP config that points the agent at the hub:
{
"mcpServers": {
"smartchats": {
"type": "http",
"url": "http://127.0.0.1:47821/mcp",
"headers": { "Authorization": "Bearer <agent token>" }
}
}
}| Tool | Notes |
|---|---|
send_message_to_user(text, artifacts?, reply_to?, ack?, status?, waiting_on?, files?) | Status defaults to update |
read_inbox(timeout_ms?, limit?) | Long-poll of unacked messages with ids. Doesn’t ack |
ack(ids) | Mark messages handled |
wait_for_message(timeout_ms?) | Next unacked message, acked when returned |
poll_messages(since?) | Retained history, no waiting |
ask_user(question, timeout_ms?) | Send as blocked on the user, then wait |
respond_and_wait(text, timeout_ms?, status?, waiting_on?) | Send, then wait. For patch-through conversations |
list_peers() | Other agents, local ones first |
send_message_to_peer(target_session_id, text, ...) | Accepts a label for a local agent |
upload_file, list_files, read_file, delete_file | Files in your SmartChats storage |
register(label?, model?), unregister() | Confirm or change identity, or leave |
hub_guide() | The guide text, also the hub://guide resource |
Long-polls default to 30 s and are capped at 10 minutes.
Configuration
Hub settings live in ~/.smartchats/hub/config.json. The file is optional. A malformed file is an error, not silently ignored.
{
"folders": { "app": "~/dev/my-app" },
"max_agents": 8,
"zellij_session": "smartchats-agents",
"remote_control": true
}| Key | Default | Notes |
|---|---|---|
folders | {} | Named folders for spawn --folder and for spawns from SmartChats |
max_agents | 8 | Running spawned agents at once |
zellij_session | smartchats-agents | Session that agents started from SmartChats open in |
remote_control | true | Offer the control session on the relay |
max_relay_agents | 10 | Relay slots while each agent has its own relay socket |
max_agent_hops | 20 | Loop guard |
offline_after_s | 600 | Liveness for agents with no known pid |
unread_alert_after_s | 600 | Unread alert on the dashboard |
dashboard | true | Serve the dashboard |
Files in the hub home:
| File | Contents |
|---|---|
hub.db | Agent registry (token hashes only) and inboxes. 500 messages and 24 h per agent |
hub.pid, hub.json | The running daemon |
admin_token | Bearer for the /admin API |
agents/<id>.mcp.json | Each agent’s MCP config, with its token (0600) |
hints.md | Editable wake hints, one ## <kind> section each |
hub_id | This hub’s id on the relay |
spawned.json, watchers/<id>.json | Spawned agents and detached watchers |
hub.log | Output under launchd |
Localhost only. The hub binds 127.0.0.1 with no option to listen elsewhere, and checks Host and Origin on every request. Anything that can read ~/.smartchats/hub/ can register agents, so keep that directory private.
Remote access
When the hub starts, it opens a control session on the relay for itself (a bridge of kind hub, labelled with your computer’s name). SmartChats uses it to list agent options, spawn agents, read an agent’s screen, send it keystrokes, and shut it down. Set "remote_control": false to turn this off.
On a relay that can host sessions, every agent’s session rides that one control socket, so the relay’s per-user connection limit counts computers rather than agents. A relay hosts up to 50 sessions per hub by default. On an older relay, or with remote_control off, each agent gets its own socket and max_relay_agents applies. status --json shows relay_sessions: { mux, hosted, own, waiting, max }.
See Relay & MCP bridge for the wire protocol.