docsHub

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 PATH

Remote 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 login

Start, 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 session

status 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.hub

The plist runs start with RunAtLoad and KeepAlive. Output goes to hub.log in the hub home.

EnvDefaultNotes
SMARTCHATS_HUB_HOME~/.smartchats/hubHub state directory (0700). --home DIR overrides it on any command
SMARTCHATS_HUB_PORT47821Fixed so agent MCP configs survive restarts
SMARTCHATS_RELAYwss://relay.kind.computerRelay endpoint
SMARTCHATS_CREDENTIALS_FILE~/.smartchats-mcp/credentials.jsonFirebase refresh token
SMARTCHATS_CLOUD_DEV_TOKENunsetSkip 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 leave

join 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]
FlagDefaultNotes
--targetclaude-code, codex or opencode. Use --launch 'CMD' for anything else
--folderA named folder from config.json
--pathcurrent directoryAny existing directory
--permissionstrustedtrusted pre-approves the folder, the hub tools and the watch command for this run only. ask adds nothing
--printoffPrint the launch line instead of opening a tab (also the behavior outside zellij)
--forceoffSpawn 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:

StatusMeans
ackReceived, starting work. The result follows
updateProgress. No one needs to act
blockedCan’t continue until someone answers. waiting_on says who: user or agent:<label>
completeThe task is done. The text is the result
failedStopped 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.

ChannelHow
smartchats-hub watch --agent IDPrints 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 WebSocketGET /events and GET /events/ws with the agent’s bearer
Long-pollsmartchats-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, toPath
SmartChats to agentrelay, hub, inbox
Dashboard to agenthub, inbox
Agent to userhub, relay, SmartChats. Copied to the dashboard
Agent to agent, same hubhub, inbox directly. No relay round trip
Agent to agent, other computerhub, 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 dashboard

The 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>" }
    }
  }
}
ToolNotes
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_fileFiles 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
}
KeyDefaultNotes
folders{}Named folders for spawn --folder and for spawns from SmartChats
max_agents8Running spawned agents at once
zellij_sessionsmartchats-agentsSession that agents started from SmartChats open in
remote_controltrueOffer the control session on the relay
max_relay_agents10Relay slots while each agent has its own relay socket
max_agent_hops20Loop guard
offline_after_s600Liveness for agents with no known pid
unread_alert_after_s600Unread alert on the dashboard
dashboardtrueServe the dashboard

Files in the hub home:

FileContents
hub.dbAgent registry (token hashes only) and inboxes. 500 messages and 24 h per agent
hub.pid, hub.jsonThe running daemon
admin_tokenBearer for the /admin API
agents/<id>.mcp.jsonEach agent’s MCP config, with its token (0600)
hints.mdEditable wake hints, one ## <kind> section each
hub_idThis hub’s id on the relay
spawned.json, watchers/<id>.jsonSpawned agents and detached watchers
hub.logOutput 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.

See also