Process architecture
In one line:main.ts calls startDaemon() (application/lifecycle.ts), which
runs preflight, starts lifecycle modules via RuntimeKernel, then launches the
transports below; ConfigManager owns config I/O and hot reload. The full
startup graph and module table live in
daemon-architecture.md.
Configuration
Most settings live in~/.monad/configs/config.json (created on first run). The daemon
port, bind address, client transport, and remote-access token are all stored there
— no env vars needed for normal use.
Transport
Two distinct axes share this name — don’t confuse them:The daemon always serves its HTTP API over two local channels at once: TCP loopback (The rest of this section covers physical transport only.
- Physical transport (this section) — how bytes travel:
tcp(HTTP over127.0.0.1) oruds(HTTP over Unix socket). Configured vianetwork.transportinconfig.json.- Semantic transport (see operation-source.md) — which ingress class produced an operation:
http(local control transports — CLI, TUI, web UI),acp(editor agents), orchannel(chat tools). This is the server-stampedSessionTransportfield in immutableOperationSourceprovenance. Bothtcpandudsconnections are classified ashttpin this sense. A narrow temporary containment gate also compares this field on selected session mutations; it is not a client-configurable access policy.
127.0.0.1:<port>) and a Unix-domain socket (~/.monad/runtime/monad.sock).
Both carry the same REST + SSE API; WebSocket push (/v1/stream) and the browser
web UI are TCP-only.
Which realtime events travel over WebSocket vs SSE — and the rule that a session’s generation stream must be subscribed explicitly over SSE, never pushed over the WS control plane — is its own decision: see realtime-channels.md.Local clients (the CLI) choose which one to dial via
network.transport in
config.json:
uds is the default everywhere: Bun supports AF_UNIX on all platforms Monad
targets — including Windows (native since Windows 10 1803) — the daemon binds the
socket on all of them, and a Unix socket is browser-safe (a web page can reach
127.0.0.1 but not an AF_UNIX path). It’s overridable at any time:
uds is selected but the socket can’t be connected (older daemon, missing
socket file, a host where the bind failed, …), the client automatically falls back
to TCP loopback for that run — the setting never makes the CLI unreachable. The
daemon likewise falls back to TCP-only if it can’t bind the socket, so it stays
reachable everywhere.
This per-OS default + automatic fallback is decided inside the transport adapter, not in feature code. This is the canonical example of the repository’s thin platform adapter rule.
Which methods speak which transport
The request/response API splits in two:-
Universal methods — reachable over both REST and all JSON-RPC transports
(WebSocket / Unix socket / stdio). These are the agent-driving surface: sessions,
agents,
tools.approve,clarify.respond,skills.list,commands.list. They are declared once in@monad/protocol’sMETHOD_TABLE; the RPC params schema and the REST verb+URL are both derived from it, so the two transports cannot drift. -
HTTP-only endpoints — REST only, no JSON-RPC twin: real-time streams
(
GET /v1/sessions/:id/eventsSSE), push (/v1/stream), and the whole management plane under/v1/settings/*(model / channels / MCP servers / ACP agents / locale) plus usage, stats, indexer, init and delegation. Settings are deliberately a REST-only management surface — an embedded stdio host drives agents, it doesn’t reconfigure the daemon. Such routes declare themselves at the controller via Elysia’sdetail.tags: ['http-only'](a fully-HTTP-only controller sets it once on the instance:new Elysia({ tags: ['http-only'] })). The route-table-parity test derives the exemption set from those tags, so there is no hand-maintained allowlist — but adding a route with neither aMETHOD_TABLEentry nor the tag fails the test on purpose (the “no silent endpoint” guard).
Environment variables
These are bootstrap-only. Everything else is inconfig.json.
In the repo’s dev setup,scripts/dev-init.tsauto-assigns a per-worktreeMONAD_PORT(andWEB_PORT) into.env.localso multiple git worktrees can runbun devat once without port clashes. That auto-assignment is dev-only tooling — it is never part of a release build; the daemon’sMONAD_PORTread is, so release users can set it by hand.
Security model
Monad is a local, single-user daemon. By default it binds the loopback interface only (127.0.0.1) plus the Unix socket under ~/.monad/runtime/ — neither
is reachable from other machines. A bound loopback port is not an exposed port.
The in-scope adversaries today are the user’s own web browser (any page can
reach 127.0.0.1) and, once tools land, the model’s own tool calls — not (yet)
a remote network attacker.
- No network exposure by default. Loopback + UDS are local-only.
- Remote access is explicit opt-in. Setting
network.remoteAccess.enabledbinds0.0.0.0and requires a bearer token (Authorization: Bearer …) for every non-loopback request. Plain-HTTP remote access sends that token in cleartext — put it behind TLS (reverse proxy / SSH tunnel / VPN); never exposehttp://0.0.0.0:<port>directly. - Agent Runtime Credentials live in
auth.json, written0600(owner-only), and are available only through protected agent execution. Native feature credentials live directly beside their owning settings; see Agent Runtime Credentials. - No-port mode.
monad --stdio/MONAD_STDIO=truetalks JSON-RPC over stdin/stdout with no port and no socket.
This is an evolving posture, not a hardened one. The loopback-trust model includes malicious local web pages in its threat scope. A loopback IP check proves only that a request came from the machine, not that the caller is authorized. Hardening includes Origin and Host validation, WebSocket origin checks, locked permissions on the socket and config.json, and tool-argument sandboxing. Contributors must follow the repository security rules before changing a network boundary, filesystem path, credential, or tool dispatch.