Skip to main content
Scope: the daemon’s outside surface — network binding, physical transports, configuration and environment variables, and the security model that follows. Everything inside the process — the startup graph, lifecycle modules, hot reload, and extension boundaries — is daemon-architecture.md’s territory. These are the deep technical details kept out of the README. Repository contributors should also follow the code-level security rules.

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:
  • Physical transport (this section) — how bytes travel: tcp (HTTP over 127.0.0.1) or uds (HTTP over Unix socket). Configured via network.transport in config.json.
  • Semantic transport (see operation-source.md) — which ingress class produced an operation: http (local control transports — CLI, TUI, web UI), acp (editor agents), or channel (chat tools). This is the server-stamped SessionTransport field in immutable OperationSource provenance. Both tcp and uds connections are classified as http in this sense. A narrow temporary containment gate also compares this field on selected session mutations; it is not a client-configurable access policy.
The rest of this section covers physical transport only.
The daemon always serves its HTTP API over two local channels at once: TCP loopback (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:
If 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’s METHOD_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/events SSE), 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’s detail.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 a METHOD_TABLE entry nor the tag fails the test on purpose (the “no silent endpoint” guard).

Environment variables

These are bootstrap-only. Everything else is in config.json.
In the repo’s dev setup, scripts/dev-init.ts auto-assigns a per-worktree MONAD_PORT (and WEB_PORT) into .env.local so multiple git worktrees can run bun dev at once without port clashes. That auto-assignment is dev-only tooling — it is never part of a release build; the daemon’s MONAD_PORT read 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.enabled binds 0.0.0.0 and 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 expose http://0.0.0.0:<port> directly.
  • Agent Runtime Credentials live in auth.json, written 0600 (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=true talks 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.