rohitg00/agentmemory · critical · ViewerConfigError

AGENTMEMORY_VIEWER_HOST=${host} requires AGENTMEMORY_SECRET

Error message

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.

What it means

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.

Source

Thrown at src/viewer/server.ts:219

  restPort?: number,
): Server {
  // Reset exported runtime state for each start attempt.
  boundViewerPort = null;
  viewerSkipped = false;

  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) {

View on GitHub (pinned to e04ba88819)

Solutions

  1. Set AGENTMEMORY_SECRET to a strong value (on Fly images it is printed on first boot — see deploy/fly/README.md)
  2. Or unset AGENTMEMORY_VIEWER_HOST to fall back to the safe loopback bind
  3. If the secret is intended, check it isn't an empty string in your env file / deployment config
  4. Send bearer tokens matching AGENTMEMORY_SECRET with viewer requests once bound publicly

Example fix

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

// after
AGENTMEMORY_VIEWER_HOST=0.0.0.0 AGENTMEMORY_SECRET=$(openssl rand -hex 32) npm run viewer
Defensive patterns

Strategy: validation

Validate before calling

const host = process.env.AGENTMEMORY_VIEWER_HOST;
if (host && !isLoopback(host) && !process.env.AGENTMEMORY_SECRET) {
  throw new Error('Refusing public viewer bind without AGENTMEMORY_SECRET');
}

Type guard

function isLoopback(host: string): boolean {
  return ['localhost', '127.0.0.1', '::1'].some(h => host === h || host.endsWith(':' + h));
}

Try / catch

try {
  await startViewerServer(options);
} catch (e) {
  if (e instanceof ViewerConfigError && e.message.includes('AGENTMEMORY_SECRET')) {
    console.error('Set AGENTMEMORY_SECRET or unset AGENTMEMORY_VIEWER_HOST');
    process.exit(1);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/626270f85c071167. Report an issue: GitHub.