abhigyanpatwari/GitNexus · critical · Error

Refusing to start eval-server on non-loopback host

Error message

Refusing to start eval-server on non-loopback host ${host} without authentication. Set GITNEXUS_AUTH_TOKEN or bind to 127.0.0.1, localhost, or ::1.

What it means

A startup security guard: assertSecureEvalServerBinding refuses to expose the eval-server query surface on a non-loopback host unless a bearer token is configured (GITNEXUS_AUTH_TOKEN from the shell, .env.local, or .env). Loopback hosts (127.0.0.1, localhost, ::1) may run unauthenticated; everything else — 0.0.0.0, LAN IPs, hostnames resolving off-box — must have auth or the process exits.

Solutions

  1. Set GITNEXUS_AUTH_TOKEN (export in the shell, or put GITNEXUS_AUTH_TOKEN=... in .env.local/.env at the server cwd) and restart
  2. Or keep it unauthenticated and bind explicitly to a loopback host: --host 127.0.0.1 (in Docker, also publish with 127.0.0.1:PORT:PORT on the host)
  3. For remote access, prefer an authenticated reverse-proxy/tunnel over widening the bind
  4. Generate a strong token: openssl rand -hex 32; clients must then send 'Authorization: Bearer <token>'

Example fix

# before
$ gitnexus eval-server --host 0.0.0.0
Error: Refusing to start eval-server on non-loopback host 0.0.0.0 without authentication.
# after
$ export GITNEXUS_AUTH_TOKEN=$(openssl rand -hex 32)
$ gitnexus eval-server --host 0.0.0.0
Defensive patterns

Strategy: validation

Validate before calling

import { isIP } from 'node:net';
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1']);
function isSafeBind(host: string, token: string | undefined): boolean {
  return Boolean(token) || LOOPBACK.has(host);
}
// check BEFORE starting the server
if (!isSafeBind(process.env.HOST ?? '127.0.0.1', process.env.GITNEXUS_AUTH_TOKEN)) {
  throw new Error('Refusing unauthenticated non-loopback bind');
}

Type guard

const isLoopbackHost = (host: string): boolean =>
  host === '127.0.0.1' || host === 'localhost' || host === '::1';

Try / catch

try {
  assertSecureEvalServerBinding(host, token);
} catch (err) {
  if (err instanceof Error && err.message.includes('non-loopback')) {
    // set GITNEXUS_AUTH_TOKEN or switch --host to 127.0.0.1, then restart
  }
}

Prevention

When it happens

Trigger: Starting eval-server with --host 0.0.0.0 or a LAN IP while GITNEXUS_AUTH_TOKEN is unset/whitespace-empty; also binding a hostname that does not resolve to a loopback address.

Common situations: Running the eval server inside Docker with -p port publishing (container binds 0.0.0.0 by default); exposing it on a LAN for another machine to query; CI containers binding all interfaces; production-style deployments reusing the dev tool.

Understand the failure class

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-08-20). Data as JSON: /api/errors/b1f170fc4c752c30. Report an issue: GitHub.

Appendix: source

Thrown at gitnexus/src/cli/eval-server.ts:155

  host: string,
  env: NodeJS.ProcessEnv,
  cwd: string = process.cwd(),
): { token?: string; warning?: string } {
  try {
    return { token: resolveEvalServerAuthToken(env, cwd) };
  } catch (error) {
    if (isEvalServerLoopbackHost(host)) {
      const reason = error instanceof Error ? error.message : String(error);
      return { warning: `${reason} Continuing without authentication on loopback host ${host}.` };
    }
    throw error;
  }
}

/** Refuse exposure of the eval-server query surface without authentication. */
export function assertSecureEvalServerBinding(host: string, authToken: string | undefined): void {
  if (!authToken && !isEvalServerLoopbackHost(host)) {
    throw new Error(
      `Refusing to start eval-server on non-loopback host ${host} without authentication. ` +
        'Set GITNEXUS_AUTH_TOKEN or bind to 127.0.0.1, localhost, or ::1.',
    );
  }
}

/** Validate the exact Bearer header while keeping token comparison constant-time. */
export function isEvalServerBearerAuthorized(
  authorization: string | string[] | undefined,
  authToken: string | undefined,
): boolean {
  if (!authToken) return true;

  const expected = Buffer.from(`Bearer ${authToken}`, 'utf8');
  const supplied = typeof authorization === 'string' ? Buffer.from(authorization, 'utf8') : null;
  const sameLength = supplied?.length === expected.length;
  const candidate = sameLength && supplied ? supplied : Buffer.alloc(expected.length);
  return crypto.timingSafeEqual(candidate, expected) && sameLength;

View on GitHub (pinned to ac9a4e9abd)