NousResearch/hermes-agent · error

Remote gateway URL is required.

Error message

Remote gateway URL is required.

What it means

ValueError from _validate_workdir (cron/jobs.py:1427): the workdir exists but is not a directory (it's a regular file, symlink-to-file, device, etc.). Cron jobs use workdir as the execution directory for AGENTS.md loading and terminal cwd, so a file target is invalid. Checked after existence so the message is precise.

Source

Thrown at apps/desktop/electron/connection-config.ts:56

const RT_COOKIE_VARIANTS = ['__Host-hermes_session_rt', '__Secure-hermes_session_rt', 'hermes_session_rt']

// The Nous portal (NAS) does NOT use Hermes gateway session cookies — it is a
// Privy-authed Next.js app. NAS `auth()` (src/server/auth/session.ts) reads the
// `privy-token` access-token cookie (with `privy-id-token` alongside), which is
// also exactly what the `/api/agents` cookie-auth path validates. So portal
// sign-in / discovery liveness must look for the Privy cookie, NOT the gateway
// cookies above. `privy-token` is the access token (the required signal);
// variants cover the secured-prefix forms and the older `privy-session` name.
const PRIVY_SESSION_COOKIE_VARIANTS = ['__Host-privy-token', '__Secure-privy-token', 'privy-token', 'privy-session']
// Keep this aligned with hermes_cli.profiles.validate_profile_name(). `default`
// is the built-in root alias; these names cannot be created as profiles.
const RESERVED_REMOTE_PROFILES = new Set(['hermes', 'test', 'tmp', 'root', 'sudo'])

function normalizeRemoteBaseUrl(rawUrl) {
  let value = String(rawUrl || '').trim()

  if (!value) {
    throw new Error('Remote gateway URL is required.')
  }

  // Users routinely paste scheme-less "host:port" (a Tailscale IP, a LAN
  // hostname). Without this, `new URL('100.64.0.1:9119')` either throws or —
  // worse — parses `host:` as the protocol and produces a baffling
  // "must be http:// or https://, got myhost:" error. Only a real
  // `scheme://` prefix opts out, so explicit non-http schemes (ftp://,
  // file://) still reach the protocol check below and get rejected.
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
    value = `http://${value}`
  }

  let parsed

  try {
    parsed = new URL(value)
  } catch (error) {
    throw new Error(`Remote gateway URL is not valid: ${error.message}`)

View on GitHub (pinned to c896c09c42)

Solutions

  1. Point workdir at the containing directory, not a file: '/home/me/proj' instead of '/home/me/proj/run.sh'.
  2. If the target should be a directory, remove the file and mkdir the directory.
  3. Double-check trailing components of long copy-pasted paths.

Example fix

# before
create_job(..., workdir="/home/me/proj/main.py")

# after
create_job(..., workdir="/home/me/proj")
Defensive patterns

Strategy: type-guard

Validate before calling

from pathlib import Path

def valid_workdir(w: str) -> bool:
    p = Path(w).expanduser()
    return p.is_absolute() and p.is_dir()  # existence + is-a-directory in one check

Type guard

from pathlib import Path

def is_workdir(p: Path) -> bool:
    """Narrows to an absolute, existing directory Path accepted by cron workdir validation."""
    return p.is_absolute() and p.exists() and p.is_dir()

Try / catch

try:
    create_job(..., workdir=w)
except ValueError as e:
    if "is not a directory" in str(e):
        # caller passed a file path — use its parent, or fix the target
        raise

Prevention

When it happens

Trigger: create_job/update_job with workdir pointing at a file — e.g. '/home/me/proj/run.sh', or a directory replaced by a file of the same name, or a broken setup where a mount point got overmounted by a file.

Common situations: User pastes a script path instead of its containing directory; a project replaced by a single archive file; path copied from docs with a filename appended by mistake.

Related errors


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