rohitg00/agentmemory · critical · ViewerConfigError

AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOST

Error message

AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOSTS because non-loopback viewer binds only trust explicit Host headers. To fix: set VIEWER_ALLOWED_HOSTS to a comma-separated list of trusted Host header values (e.g. "localhost:3113" for fly proxy), or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind.

What it means

startViewerServer throws ViewerConfigError when a non-loopback AGENTMEMORY_VIEWER_HOST bind has a secret but VIEWER_ALLOWED_HOSTS is empty. Non-loopback binds only trust explicit Host header allowlist values to prevent DNS-rebinding/Host-header attacks against the proxy; without an allowlist the server refuses to start.

Source

Thrown at src/viewer/server.ts:224

  const resolvedRestPort = restPort ?? port - 2;
  const requestedPort = port;
  const host = resolveViewerHost();
  let inboundSecret: string | null = null;

  // Non-loopback bind turns the viewer into a network-reachable
  // bearer-authorized proxy. Refuse to start unless the operator has
  // both an inbound secret to authenticate callers against and an
  // explicit Host header allowlist; otherwise the listening socket
  // becomes an open relay to the local REST API.
  if (!isLoopbackHost(host)) {
    if (!secret) {
      throw new ViewerConfigError(
        `AGENTMEMORY_VIEWER_HOST=${host} requires AGENTMEMORY_SECRET to be set so the viewer can validate inbound bearer tokens. To fix: unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind, or set AGENTMEMORY_SECRET. For Fly images, it is printed on first boot; see deploy/fly/README.md.`,
      );
    }
    if (readAllowedHostsOverride().length === 0) {
      throw new ViewerConfigError(
        `AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOSTS because non-loopback viewer binds only trust explicit Host headers. To fix: set VIEWER_ALLOWED_HOSTS to a comma-separated list of trusted Host header values (e.g. "localhost:3113" for fly proxy), or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind.`,
      );
    }
    inboundSecret = secret;
  }

  // Computed lazily on first request — `port` may be 0 here (OS-assigned)
  // or the EADDRINUSE retry loop below may bump us to a different port,
  // so we read the actual bound port from server.address() on first hit.
  let allowedHosts: Set<string> | null = null;

  const server = createServer(async (req, res) => {
    if (!allowedHosts) {
      const addr = server.address();
      const actualPort =
        addr && typeof addr === "object" && "port" in addr
          ? (addr.port as number)
          : port;

View on GitHub (pinned to e04ba88819)

Solutions

  1. Set VIEWER_ALLOWED_HOSTS to the exact Host header values clients send, comma-separated (e.g. VIEWER_ALLOWED_HOSTS="localhost:3113" or "your-app.fly.dev")
  2. Or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind
  3. Verify parsing: no stray commas/quotes; the list must be non-empty after trim
  4. Update the allowlist after app renames or custom-domain changes

Example fix

// before
AGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=... npm run viewer // ViewerConfigError

// after
AGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=... VIEWER_ALLOWED_HOSTS="localhost:3113,myapp.fly.dev" npm run viewer
Defensive patterns

Strategy: validation

Validate before calling

const host = process.env.AGENTMEMORY_VIEWER_HOST;
if (host && !isLoopback(host)) {
  const allowed = (process.env.VIEWER_ALLOWED_HOSTS ?? '').split(',').map(s => s.trim()).filter(Boolean);
  if (allowed.length === 0) throw new Error('Non-loopback viewer bind needs VIEWER_ALLOWED_HOSTS');
}

Type guard

function hasAllowedHosts(v: string | undefined): v is string {
  return !!v && v.split(',').map(s => s.trim()).filter(Boolean).length > 0;
}

Try / catch

try {
  await startViewerServer(options);
} catch (e) {
  if (e instanceof ViewerConfigError && e.message.includes('VIEWER_ALLOWED_HOSTS')) {
    console.error('Set VIEWER_ALLOWED_HOSTS (e.g. "localhost:3113,app.fly.dev") or unset AGENTMEMORY_VIEWER_HOST');
    process.exit(1);
  }
  throw e;
}

Prevention

When it happens

Trigger: Starting the viewer with AGENTMEMORY_VIEWER_HOST set to a public host and AGENTMEMORY_SECRET set, while readAllowedHostsOverride() returns [] (VIEWER_ALLOWED_HOSTS unset or only empty/comma tokens) — the second guard in the same isLoopbackHost branch fires.

Common situations: Fly.io deploys where the operator sets the secret but forgets the Host allowlist the fly proxy sends (e.g. 'localhost:3113' or the app's fly.dev hostname); trailing whitespace/quotes or a single comma making the parsed list empty; renaming the app so the old allowed host no longer matches; local .env reused for remote deploys without VIEWER_ALLOWED_HOSTS.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of rohitg00/agentmemory@e04ba88819 (2026-08-30). Data as JSON: /api/errors/072242d302769aa0. Report an issue: GitHub.