{"record":{"id":"626270f85c071167","repo":"rohitg00/agentmemory","slug":"agentmemory-viewer-host-host-requires-agentmemo","errorCode":null,"errorMessage":"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.","messagePattern":"AGENTMEMORY_VIEWER_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\\.","errorType":"exception","errorClass":"ViewerConfigError","httpStatus":null,"severity":"critical","filePath":"src/viewer/server.ts","lineNumber":219,"sourceCode":"  restPort?: number,\n): Server {\n  // Reset exported runtime state for each start attempt.\n  boundViewerPort = null;\n  viewerSkipped = false;\n\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) {","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/rohitg00/agentmemory/blob/e04ba88819c365c9acf9d6661ea802143e728bd6/src/viewer/server.ts#L201-L237","documentation":"startViewerServer throws ViewerConfigError when AGENTMEMORY_VIEWER_HOST is set to a non-loopback host but AGENTMEMORY_SECRET is unset. A non-loopback bind exposes the viewer as a bearer-authenticated proxy to the local REST API, so without an inbound secret the socket would be an open relay; the server refuses to start. The message names both fixes: unset the host override or provide the secret.","triggerScenarios":"Starting the viewer (viewerServer/viewer/server commands) with AGENTMEMORY_VIEWER_HOST set to a public/LAN interface (e.g. 0.0.0.0 or a Fly.io IP) while AGENTMEMORY_SECRET is empty or unset — isLoopbackHost(host) returns false and the first guard fires.","commonSituations":"Deploying to Fly.io and setting the public bind without copying the boot-printed AGENTMEMORY_SECRET; exposing the viewer on LAN for team access without configuring auth; typo'd or empty AGENTMEMORY_SECRET value; running the same local config on a remote host.","solutions":["Set AGENTMEMORY_SECRET to a strong value (on Fly images it is printed on first boot — see deploy/fly/README.md)","Or unset AGENTMEMORY_VIEWER_HOST to fall back to the safe loopback bind","If the secret is intended, check it isn't an empty string in your env file / deployment config","Send bearer tokens matching AGENTMEMORY_SECRET with viewer requests once bound publicly"],"exampleFix":"// before\nAGENTMEMORY_VIEWER_HOST=0.0.0.0 npm run viewer // ViewerConfigError\n\n// after\nAGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=$(openssl rand -hex 32) npm run viewer","handlingStrategy":"validation","validationCode":"const host = process.env.AGENTMEMORY_VIEWER_HOST;\nif (host && !isLoopback(host) && !process.env.AGENTMEMORY_SECRET) {\n  throw new Error('Refusing public viewer bind without AGENTMEMORY_SECRET');\n}","typeGuard":"function isLoopback(host: string): boolean {\n  return ['localhost', '127.0.0.1', '::1'].some(h => host === h || host.endsWith(':' + h));\n}","tryCatchPattern":"try {\n  await startViewerServer(options);\n} catch (e) {\n  if (e instanceof ViewerConfigError && e.message.includes('AGENTMEMORY_SECRET')) {\n    console.error('Set AGENTMEMORY_SECRET or unset AGENTMEMORY_VIEWER_HOST');\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Keep viewer binds loopback in development; only go public in deliberate deploys","Provision AGENTMEMORY_SECRET in the deployment platform (Fly prints it on first boot)","Add an env preflight script that fails deploys missing public-bind prerequisites","Never ship empty-string AGENTMEMORY_SECRET values in env files"],"tags":["security","config","env-var","auth","viewer"],"backgroundTag":"missing-env-var","analyzedSha":"e04ba88819c365c9acf9d6661ea802143e728bd6","analyzedAt":"2026-08-30T01:07:40.754Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}