NousResearch/hermes-agent · error · Error

Gateway rejected native login: ${error}${desc ? ` (${desc})`

Error message

Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''}

What it means

Part of the native (RFC 8252 loopback) OAuth flow: after the gateway redirects back to 127.0.0.1 with ?code=...&state=..., parseLoopbackCallback checks for an OAuth 2.0 'error' query parameter. If present, the gateway explicitly rejected the login and the error code (plus error_description) is rethrown here. This is the gateway speaking, not a desktop bug — common codes are access_denied, invalid_request, unauthorized_client.

Source

Thrown at apps/desktop/electron/native-oauth.ts:154

  return `${parsed.protocol}//${parsed.host}${prefix}/auth/native/refresh`
}

/**
 * Parse the loopback redirect the gateway sends the browser to. Returns the
 * `code` + `state`, or throws with the gateway's `error` if the flow failed.
 * `expectedState` MUST match (CSRF defense — RFC 6749 §10.12); a mismatch
 * throws rather than proceeding.
 */
export function parseLoopbackCallback(requestUrl: string, expectedState: string): { code: string } {
  // requestUrl is the path+query the loopback server received, e.g.
  // "/callback?code=...&state=...". Resolve against a dummy origin to parse.
  const parsed = new URL(requestUrl, 'http://127.0.0.1')
  const error = parsed.searchParams.get('error')

  if (error) {
    const desc = parsed.searchParams.get('error_description') || ''
    throw new Error(`Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''}`)
  }

  const code = parsed.searchParams.get('code') || ''
  const state = parsed.searchParams.get('state') || ''

  if (!code) {
    throw new Error('Loopback callback missing authorization code')
  }

  if (!expectedState || state !== expectedState) {
    // Never redeem a code that arrived with a mismatched state — it may be a
    // forged callback trying to inject an attacker's code.
    throw new Error('Loopback callback state mismatch (possible CSRF)')
  }

  return { code }
}

View on GitHub (pinned to c896c09c42)

Solutions

  1. Read the error code and description in the message — access_denied means user refusal (retry and approve), others point at gateway/provider config
  2. Verify the gateway's OAuth client registration: redirect_uri must be the loopback origin the desktop uses
  3. Confirm the gateway supports/enables the native (loopback) flow; otherwise use the browser-cookie flow or a token
  4. Check gateway logs at the moment of the redirect for the authorize failure reason
Defensive patterns

Strategy: try-catch

Type guard

function isGatewayLoginRejection(e: unknown): e is Error {
  return e instanceof Error && e.message.startsWith('Gateway rejected native login:')
}

Try / catch

try { const { code } = parseLoopbackCallback(requestUrl, expectedState) } catch (e) { if (isGatewayLoginRejection(e)) { surfaceAuthError(e.message); abortFlow() } else throw e }

Prevention

When it happens

Trigger: The gateway's authorize endpoint redirects to the loopback callback with ?error=access_denied (user denied consent), ?error=invalid_request (malformed authorize request), or a provider-side failure — the desktop then parses that redirect and throws.

Common situations: User clicks 'deny' on the consent screen; the OAuth client_id/redirect_uri registered on the gateway doesn't match what the desktop sent; upstream IdP outage; attempting native login when the gateway has native flow disabled.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/d33f2a28bc829a0c. Report an issue: GitHub.