NousResearch/hermes-agent

Auth API key crash in non-interactive environments

Cluster 10468. Data as of

Nothing open on GitHub. Fix merged in #10599 on 16 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #10599. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

A bug where hermes auth add api_key crashes when a label prompt is shown in non-interactive environments, along with related CLI quality-of-life fixes. The fix involves silently using a default label when no interactive input is available.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #10599 fix: three CLI quality-of-life fixes

    merged to main

    files: hermes_cli/auth_commands.py, hermes_cli/main.py, hermes_cli/model_switch.py

    Show the diff
    • hermes_cli/auth_commands.py
      @@ -4,6 +4,7 @@
       
       from getpass import getpass
       import math
      +import sys
       import time
       from types import SimpleNamespace
       import uuid
      @@ -160,7 +161,10 @@ def auth_add_command(args) -> None:
               default_label = _api_key_default_label(len(pool.entries()) + 1)
               label = (getattr(args, "label", None) or "").strip()
               if not label:
      -            label = input(f"Label (optional, default: {default_label}): ").strip() or default_label
      +            if sys.stdin.isatty():
      +                label = input(f"Label (optional, default: {default_label}): ").strip() or default_label
      +            else:
      +                label = default_label
               entry = PooledCredential(
                   provider=provider,
                   id=uuid.uuid4().hex[:6],
    • hermes_cli/main.py
      @@ -6325,8 +6325,13 @@ def cmd_acp(args):
                   sys.stderr = _io.StringIO()
                   args = parser.parse_args(_processed_argv)
                   sys.stderr = _saved_stderr
      -        except SystemExit:
      +        except SystemExit as exc:
                   sys.stderr = _saved_stderr
      +            # Help/version flags (exit code 0) already printed output —
      +            # re-raise immediately to avoid a second parse_args printing
      +            # the same help text again (#10230).
      +            if exc.code == 0:
      +                raise
                   # Subcommand name was consumed as a flag value (e.g. -c model).
                   # Fall back to optional subparsers so argparse handles it normally.
                   subparsers.required = False
    • hermes_cli/model_switch.py
      @@ -786,7 +786,8 @@ def list_authenticated_providers(
           from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS
       
           results: List[dict] = []
      -    seen_slugs: set = set()
      +    seen_slugs: set = set()  # lowercase-normalized to catch case variants (#9545)
      +    seen_mdev_ids: set = set()  # prevent duplicate entries for aliases (e.g. kimi-coding + kimi-coding-cn)
       
           data = fetch_models_dev()
       
      @@ -799,6 +800,11 @@ def list_authenticated_providers(
       
           # --- 1. Check Hermes-mapped providers ---
           for hermes_id, mdev_id in PROVIDER_TO_MODELS_DEV.items():
      +        # Skip aliases that map to the same models.dev provider (e.g.
      +        # kimi-coding and kimi-coding-cn both → kimi-for-coding).
      +        # The first one with valid credentials wins (#10526).
      +        if mdev_id in seen_mdev_ids:
      +            continue
               pdata = data.get(mdev_id)
               if not isinstance(pdata, dict):
                   continue
      @@ -837,7 +843,8 @@ def list_authenticated_providers(
                   "total_models": total,
                   "source": "built-in",
               })
      -        seen_slugs.add(slug)
      +        seen_slugs.add(slug.lower())
      +        seen_mdev_ids.add(mdev_id)
       
           # --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---
           from hermes_cli.providers import HERMES_OVERLAYS
      @@ -849,12 +856,12 @@ def list_authenticated_providers(
           _mdev_to_hermes = {v: k for k, v in PROVIDER_TO_MODELS_DEV.items()}
       
           for pid, overlay in HERMES_OVERLAYS.items():
      -        if pid in seen_slugs:
      +        if pid.lower() in seen_slugs:
                   continue
       
               # Resolve Hermes slug — e.g. "github-copilot" → "copilot"
               hermes_slug = _mdev_to_hermes.get(pid, pid)
      -        if hermes_slug in seen_slugs:
      +        if hermes_slug.lower() in seen_slugs:
                   continue
       
               # Check if credentials exist
      @@ -935,8 +942,8 @@ def list_authenticated_providers(
                   "total_models": total,
                   "source": "hermes",
               })
      -        seen_slugs.add(pid)
      -        seen_slugs.add(hermes_slug)
      +        seen_slugs.add(pid.lower())
      +        seen_slugs.add(hermes_slug.lower())
       
           # --- 2b. Cross-check canonical provider list ---
           # Catches providers that are in CANONICAL_PROVIDERS but weren't found
      @@ -948,7 +955,7 @@ def list_authenticated_providers(
               _canon_provs = []
       
           for _cp in _canon_provs:
      -        if _cp.slug in seen_slugs:
      +        if _cp.slug.lower() in seen_slugs:
                   continue
       
               # Check credentials via PROVIDER_REGISTRY (auth.py)
      @@ -995,7 +1002,7 @@ def list_authenticated_providers(
                   "total_models": _cp_total,
                   "source": "canonical",
               })
      -        seen_slugs.add(_cp.slug)
      +        seen_slugs.add(_cp.slug.lower())
       
           # --- 3. User-defined endpoints from config ---
           if user_providers and isinstance(user_providers, dict):
      @@ -1068,7 +1075,7 @@ def list_authenticated_providers(
                       groups[slug]["models"].append(default_model)
       
               for slug, grp in groups.items():
      -            if slug in seen_slugs:
      +            if slug.lower() in seen_slugs:
                       continue
                   results.append({
                       "slug": slug,
      @@ -1080,7 +1087,7 @@ def list_authenticated_providers(
                       "source": "user-config",
                       "api_url": grp["api_url"],
                   })
      -            seen_slugs.add(slug)
      +            seen_slugs.add(slug.lower())
       
           # Sort: current provider first, then by model count descending
           results.sort(key=lambda r: (not r["is_current"], -r["total_models"]))

1 PR closed without merging

From the close comments: merged via another change 1

  • #10475 fix(auth): use default label silently in non-interactive environments · merged via another change → b750c72, #10468, #10230, #10526, #9545, #10599
Closed issues · 1
  • Closed#10468Bug: hermes auth add api_key crashes on non-interactive label prompt
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#10599
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
16 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.