{"record":{"id":"072242d302769aa0","repo":"rohitg00/agentmemory","slug":"agentmemory-viewer-host-host-requires-viewer-al","errorCode":null,"errorMessage":"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.","messagePattern":"AGENTMEMORY_VIEWER_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\\.","errorType":"exception","errorClass":"ViewerConfigError","httpStatus":null,"severity":"critical","filePath":"src/viewer/server.ts","lineNumber":224,"sourceCode":"\n  const resolvedRestPort = restPort ?? port - 2;\n  const requestedPort = port;\n  const host = resolveViewerHost();\n  let inboundSecret: string | null = null;\n\n  // Non-loopback bind turns the viewer into a network-reachable\n  // bearer-authorized proxy. Refuse to start unless the operator has\n  // both an inbound secret to authenticate callers against and an\n  // explicit Host header allowlist; otherwise the listening socket\n  // becomes an open relay to the local REST API.\n  if (!isLoopbackHost(host)) {\n    if (!secret) {\n      throw new ViewerConfigError(\n        `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.`,\n      );\n    }\n    if (readAllowedHostsOverride().length === 0) {\n      throw new ViewerConfigError(\n        `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.`,\n      );\n    }\n    inboundSecret = secret;\n  }\n\n  // Computed lazily on first request — `port` may be 0 here (OS-assigned)\n  // or the EADDRINUSE retry loop below may bump us to a different port,\n  // so we read the actual bound port from server.address() on first hit.\n  let allowedHosts: Set<string> | null = null;\n\n  const server = createServer(async (req, res) => {\n    if (!allowedHosts) {\n      const addr = server.address();\n      const actualPort =\n        addr && typeof addr === \"object\" && \"port\" in addr\n          ? (addr.port as number)\n          : port;","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/rohitg00/agentmemory/blob/e04ba88819c365c9acf9d6661ea802143e728bd6/src/viewer/server.ts#L206-L242","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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\")","Or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind","Verify parsing: no stray commas/quotes; the list must be non-empty after trim","Update the allowlist after app renames or custom-domain changes"],"exampleFix":"// before\nAGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=... npm run viewer // ViewerConfigError\n\n// after\nAGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=... VIEWER_ALLOWED_HOSTS=\"localhost:3113,myapp.fly.dev\" npm run viewer","handlingStrategy":"validation","validationCode":"const host = process.env.AGENTMEMORY_VIEWER_HOST;\nif (host && !isLoopback(host)) {\n  const allowed = (process.env.VIEWER_ALLOWED_HOSTS ?? '').split(',').map(s => s.trim()).filter(Boolean);\n  if (allowed.length === 0) throw new Error('Non-loopback viewer bind needs VIEWER_ALLOWED_HOSTS');\n}","typeGuard":"function hasAllowedHosts(v: string | undefined): v is string {\n  return !!v && v.split(',').map(s => s.trim()).filter(Boolean).length > 0;\n}","tryCatchPattern":"try {\n  await startViewerServer(options);\n} catch (e) {\n  if (e instanceof ViewerConfigError && e.message.includes('VIEWER_ALLOWED_HOSTS')) {\n    console.error('Set VIEWER_ALLOWED_HOSTS (e.g. \"localhost:3113,app.fly.dev\") or unset AGENTMEMORY_VIEWER_HOST');\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Mirror the exact Host header the reverse proxy sends (fly proxy sends localhost:3113) into VIEWER_ALLOWED_HOSTS","Update the allowlist when renaming apps or adding custom domains","Validate the parsed allowlist is non-empty after trimming/quote-stripping","Prefer the default loopback bind unless a public viewer is explicitly required"],"tags":["security","config","env-var","host-header","viewer"],"backgroundTag":"missing-env-var","analyzedSha":"e04ba88819c365c9acf9d6661ea802143e728bd6","analyzedAt":"2026-08-30T01:07:40.754Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}