HTTP API Reference

ccmuxd (the ccmux daemon) exposes a small HTTP + JSON API. It’s how the ccmux TUI talks to a remote machine over Tailscale, and it’s the integration surface for mobile clients — like the Moshi app — that want to list, attach to, and act on agent sessions without shelling in.

This page is the contract. It’s generated from cmd/ccmuxd/main.go (routes) and internal/daemon/protocol.go (the request/response types) in the ccmux repo. If the running daemon disagrees with this page, the daemon wins — please file an issue.

Transport & reachability

The same routes are served over two transports:

TransportAddressDefault
Local Unix socket~/.local/state/ccmux/ccmuxd.sock (mode 0600)always on
Tailnet HTTPhttp://TAILNET-IPv4:PORT (port default 7474)off

To reach a daemon from another device, on the daemon host: set daemon.listen_tailnet = true (optionally daemon.tailnet_port) in ~/.config/ccmux/config.toml and restart ccmuxd. The bind address is the host’s tailscale ip -4. If Tailscale isn’t running, the tailnet listener silently doesn’t start (only the Unix socket is served).

Authentication & trust model — read this first

There is no application-level authentication on the HTTP API. No bearer token, no API key, no per-request signature, no IP allowlist. The trust boundary is your Tailscale tailnet: any host that can reach the daemon’s 100.x.x.x:7474 can call every endpoint — list / create / kill / rename / send-keys, attach to a full interactive terminal, and read notes / conversations / usage.

Secure this with Tailscale ACLs, not app-level auth. Treat anything that can route to the daemon’s tailnet IP as fully trusted.

Two endpoints validate per request, and only to bootstrap device trust + push — not to protect the API:

Error shape

Errors are plain text (text/plain): a single-line message, not a JSON envelope. Check the status code, and read the body as a string for the human message. Successful JSON responses are application/json.

CodeMeaning
200OK (JSON body)
204OK, no body (kill / send-keys / device register)
400bad input / validation failure
401invalid or expired pairing token (/v1/pair only)
404not found — or this daemon predates the endpoint
405wrong HTTP method
500server / tmux / scaffold failure
503feature unavailable (e.g. /v1/pair-token with listen_tailnet off)

The API evolves by adding JSON fields (all optional fields use omitempty) and adding routes. Treat a 404 on a whole endpoint as “this daemon is older than that feature” and degrade gracefully.

Endpoint summary

MethodPathResponse
GET/v1/healthHealthInfo
GET/v1/peers[]PeerInfo
GET/v1/sessions[]SessionState
POST/v1/sessionsSessionState (create-or-attach)
POST/v1/sessions/bareNewBareSessionResponse
POST/v1/sessions/NAME/kill204
POST/v1/sessions/NAME/renameSessionState
POST/v1/sessions/NAME/send-keys204
GET/v1/sessions/NAME/previewPreviewResponse
GET/v1/sessions/NAME/attachWebSocket (interactive PTY)
GET/v1/projects[]ProjectInfo
POST/v1/projectsNewProjectResponse
GET/v1/conversations[]Conversation
GET/v1/usage?window=…AgentUsage
GET/v1/notes?project=…[&file=…][]NoteEntry or NoteContent
GET/v1/notes/search?project=…&q=…[]SearchHit
GET/v1/eventsSSE stream of SessionEvent
POST/v1/pair-token (Unix only)PairTokenResponse
POST/v1/pairPairResponse
POST/v1/devices204
POST/v1/devices/test204

(Where a path shows NAME it’s the tmux session name path segment, i.e. /v1/sessions/{name}/kill.)

Endpoint details

Sessions

Interactive terminal — GET /v1/sessions/NAME/attach (WebSocket)

Upgrade to a WebSocket bridged to a real tmux attach-session in a PTY: a true interactive terminal (live output, input, resize) without ssh/mosh. Uses coder/websocket framing.

Initial PTY size is 80x24 until the first resize. The server pings every 25s with a 10s deadline — answer pongs or expect a teardown. InsecureSkipVerify is set (no Origin check). Closing the socket only detaches; the tmux session keeps running. Prefer this over polling /preview for an interactive view.

Projects

Conversations, usage, notes

Live updates — GET /v1/events (SSE)

text/event-stream. Each data: frame is a JSON SessionEvent; kind is created / killed / state_change / needs_input. Heartbeats: : connected on open, : ping every 20s (comment lines starting with : are ignorable). An event: drops / data: N frame means you missed N events and should re-fetch /v1/sessions to resync.

Health & discovery

Pairing & push (mobile)

Optional flow for native push (APNs/iOS, FCM/Android) and installing an SSH key so the device can ssh/mosh attach. Not required to use the read/act endpoints over the tailnet.

Pushes fire on two transitions: a session entering needs_input, and active → idle (“agent finished”). The push’s session id is local/SESSIONNAME. APNs and FCM are off by default and need server-side config; FCM routing exists but Android delivery isn’t wired up yet.

Types

Copied from internal/daemon/protocol.go (Go structs with their JSON tags).

type SessionState struct {
	Name        string    `json:"name"`
	Host        string    `json:"host"`         // "local" or a remote host name
	Project     string    `json:"project"`
	Path        string    `json:"path"`         // session working directory
	State       string    `json:"state"`        // active|idle|needs_input|error|unknown
	Attached    bool      `json:"attached"`
	Windows     int       `json:"windows"`
	Created     time.Time `json:"created"`
	LastChange  time.Time `json:"last_change"`
	PromptCount int       `json:"prompt_count"`
	Agent       string    `json:"agent,omitempty"`
}

type HealthInfo struct {
	OK        bool   `json:"ok"`
	Hostname  string `json:"hostname"`
	Version   string `json:"version"`
	Sessions  int    `json:"sessions"`
	SleepMode string `json:"sleep_mode"` // off|safe|dangerous|very_dangerous
}

type SessionEvent struct {
	At      time.Time    `json:"at"`
	Kind    string       `json:"kind"` // state_change|created|killed|needs_input
	Session SessionState `json:"session"`
}

type NewSessionRequest struct {
	Project  string `json:"project"`
	Path     string `json:"path"`     // defaults to PROJECTS_ROOT/PROJECT
	Continue bool   `json:"continue"` // start the agent with --continue
	Name     string `json:"name,omitempty"`
	Agent    string `json:"agent,omitempty"`
}

type NewBareSessionRequest struct {
	Name  string `json:"name,omitempty"`
	Path  string `json:"path,omitempty"`
	Agent string `json:"agent,omitempty"`
}
type NewBareSessionResponse struct {
	Session string `json:"session"`
	Path    string `json:"path"`
	Host    string `json:"host"`
}

type NewProjectRequest struct {
	Name  string `json:"name"`
	Agent string `json:"agent,omitempty"`
}
type NewProjectResponse struct {
	Session string `json:"session"`
	Path    string `json:"path"`
	Host    string `json:"host"`
}

type ProjectInfo struct {
	Name      string    `json:"name"`
	Host      string    `json:"host"`
	Path      string    `json:"path"` // absolute on the daemon host
	HasGit    bool      `json:"has_git"`
	HasCM     bool      `json:"has_cm"`
	HasAgents bool      `json:"has_agents,omitempty"`
	HasDocs   bool      `json:"has_docs"`
	Agent     string    `json:"agent,omitempty"`
	Modified  time.Time `json:"modified"`
}

type PeerInfo struct {
	Hostname   string `json:"hostname"`
	Addr       string `json:"addr"` // tailnet IPv4
	OS         string `json:"os"`
	Online     bool   `json:"online"`
	RunsCCMuxd bool   `json:"runs_ccmuxd"`
	Port       *int   `json:"port,omitempty"`
}

type Conversation struct {
	ID       string    `json:"id"` // agent UUID (its --resume id)
	Agent    string    `json:"agent"`
	Project  string    `json:"project,omitempty"`
	Path     string    `json:"path,omitempty"`
	Preview  string    `json:"preview,omitempty"`
	Modified time.Time `json:"modified"`
}

type AgentUsage struct {
	Claude      UsageSummary `json:"claude"`
	Codex       UsageSummary `json:"codex"`
	Antigravity UsageSummary `json:"antigravity"`
}
type UsageSummary struct {
	HasData       bool    `json:"has_data"`
	WindowSeconds int     `json:"window_seconds"`
	Prompts       int     `json:"prompts"`
	InputTokens   int     `json:"input_tokens"`
	OutputTokens  int     `json:"output_tokens"`
	EstimatedCost float64 `json:"estimated_cost"` // USD
}

type NoteEntry struct {
	Rel      string    `json:"rel"`
	Dir      string    `json:"dir"`
	Display  string    `json:"display"`
	Modified time.Time `json:"modified"`
}
type NoteContent struct {
	Rel     string `json:"rel"`
	Content string `json:"content"`
}
type SearchHit struct {
	Rel     string `json:"rel"`
	LineNum int    `json:"line_num"`
	Snippet string `json:"snippet"`
}

type PreviewResponse struct {
	Lines   int    `json:"lines"`
	Content string `json:"content"`
}

type RenameRequest   struct{ Name string `json:"name"` }
type SendKeysRequest struct{ Keys string `json:"keys"` }

type PairRequest struct {
	Token       string `json:"token"`
	PublicKey   string `json:"public_key"`
	DeviceToken string `json:"device_token,omitempty"`
	APNsEnv     string `json:"apns_env,omitempty"` // development|production
}
type PairResponse      struct{ Hostname string `json:"hostname"`; Version string `json:"version"` }
type PairTokenResponse struct{ Token string `json:"token"`; URL string `json:"url"` }
type RegisterDeviceRequest struct {
	Token     string `json:"token"`
	Env       string `json:"env,omitempty"` // apns: development|production; fcm: empty
	PublicKey string `json:"public_key"`
	Provider  string `json:"provider,omitempty"` // apns (default) | fcm
}
// POST /v1/devices/test body is { "public_key": "..." }

Validation rules to mirror client-side

Examples

# Health (over the tailnet)
curl -s http://100.75.64.20:7474/v1/health | jq

# List sessions
curl -s http://100.75.64.20:7474/v1/sessions | jq

# Type a reply into a session and press Enter
curl -s -X POST http://100.75.64.20:7474/v1/sessions/c-auth/send-keys \
  -H 'content-type: application/json' \
  -d '{"keys":"yes, ship it\n"}'

# Peek at the last 40 lines of a pane
curl -s 'http://100.75.64.20:7474/v1/sessions/c-auth/preview?lines=40' | jq -r .content

# Subscribe to live events
curl -sN http://100.75.64.20:7474/v1/events

The Go reference client in internal/daemon/client.go is the canonical consumer and a useful map of method → endpoint.


Spotted an error or something out of date? Edit this page on GitHub.