By default CALM talks to its client over stdio (or a local unix socket in
shared-daemon mode — see docs/mcp-client-setup.md). calm serve --http
adds a third, opt-in transport: Streamable-HTTP, the same protocol
Claude Code and other clients speak to remote MCP servers.
This exists for one narrow use case: a devcontainer or GitHub Codespace, where the editor/agent runs on your machine but the project (and therefore the index) lives inside the container, and stdio/unix-socket forwarding isn't available across that boundary. It is not a general-purpose "expose CALM to the internet" feature, and the defaults below are deliberately conservative about being used that way.
cargo build --features http
A binary built without this feature still accepts --http on the command
line (so --help output doesn't silently omit it) but refuses to start:
--http requires building with --features http (this binary wasn't).
calm serve --project-root . --http
# binds 127.0.0.1:8787 by default (--addr to override)
A bind address whose IP is not loopback (127.0.0.1/::1) is refused
unless --allow-remote is also passed:
$ calm serve --http --addr 0.0.0.0:8787
error: refusing non-loopback HTTP bind 0.0.0.0:8787 without --allow-remote
(fail-closed default -- see docs/http-transport.md)
This is intentional and fail-closed: the common devcontainer/Codespace case
only ever needs a loopback bind (the port is forwarded to your local
machine by the container tooling itself, not exposed on the container's own
network interface), so that's what works with zero extra flags. Reaching
for --allow-remote should be a deliberate, informed decision, not an
accident of binding 0.0.0.0 out of habit.
CALM_HTTP_TOKEN=$(openssl rand -hex 32) \
calm serve --http --addr 0.0.0.0:8787 --allow-remote
Two things happen the moment a bind is non-loopback, and neither is optional:
CALM_HTTP_TOKENmust be set to a non-empty value, or the server refuses to start at all — an unauthenticated remote bind is refused before it ever opens a socket, not left to a middleware layer to catch after the fact. Every request then needsAuthorization: Bearer <that token>or gets401 Unauthorized.- The effective preset is forced to
remote-safe(every tool that declaresread_only_hint = true), regardless of what--presetrequested. This is a capability check, not a toolset-name exclusion: it covers not just the obvious write path (edit_lines,edit_symbol,format_files) but every other state-mutating or process-executing tool too —remember(writes durable memory),verify_change/retry_maintenance(the latter spawnscargo check),scip_refresh/lsp_refresh(run external provider processes),set_toolset(mutates session state),pattern_debt_register. None of those are network-reachable via this transport by default. There is currently no flag to override this; if you need remote edit (or memory-write, or verification-triggering) access, you're outside this feature's intended scope and should reconsider the setup instead (e.g. run CALM inside the same trust boundary as the client).
A loopback bind needs neither: no token check, and whatever --preset you
asked for.
The bearer-token check is a coarse gate against an unauthenticated client
reaching the tool surface — it is not a substitute for TLS. The token
travels in plaintext over whatever transport carries the HTTP request; if
--allow-remote traffic crosses a network you don't fully trust, put a
real reverse proxy (nginx, Caddy, your cloud provider's load balancer) in
front that terminates TLS, and bind CALM itself to loopback behind it —
the proxy talks to 127.0.0.1:8787, only the proxy's TLS-terminated
endpoint is actually remote-facing. CALM does not attempt to be its own
TLS-terminating edge server.
- Rate limiting / DoS protection — defense-in-depth only, not a real
policy.
serve_httpcaps request body size (16 MiB) and concurrent in-flight requests (64) so a malformed or flooding client can't exhaust memory or spawn unbounded concurrent sessions, but there's no per-IP rate limiting, no backoff, no request queueing. This is a single-tenant dev-loop tool, not a public service — put a real reverse proxy in front (see above) if you need actual rate limiting. - Per-request audit detail —
serve_http's session-accept audit log (.calm/audit.login daemon mode) doesn't currently carry the remote peer's IP; seecrates/calm-server/src/http.rs's doc comment for why (the service-factory seam it hooks doesn't have per-request access). - Token rotation — restart the process with a new
CALM_HTTP_TOKENto rotate; there's no live-reload for it.