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
- 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
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
- 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
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
- AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOST
- OPENAI_API_KEY is required for the openai provider
- Refusing to read image outside managed store: ${data.raw.ima
- ${envName} must be a positive integer, got: ${override}
- EEXIST
AI-assisted analysis of rohitg00/agentmemory@e04ba88819 (2026-08-30).
Data as JSON: /api/errors/626270f85c071167.
Report an issue: GitHub.