# NousResearch/hermes-agent · Auth API key crash in non-interactive environments

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #10599. Verify it addresses your case.
- **Recommended:** read PR #10599 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #10468: Bug: hermes auth add api_key crashes on non-interactive label prompt

## Prior & competing PRs

- **#10599: fix: three CLI quality-of-life fixes**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/auth_commands.py`, `hermes_cli/main.py`, `hermes_cli/model_switch.py`
  - fix hunks:
  - `hermes_cli/auth_commands.py`

    ```diff
    @@ -4,6 +4,7 @@
     
     from getpass import getpass
     import math
    +import sys
     import time
     from types import SimpleNamespace
     import uuid
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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()
     
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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)
    ```

    ```diff
    @@ -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):
    ```

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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"]))
    ```
- **#10475: fix(auth): use default label silently in non-interactive environments**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `b750c720cdd3a0c04d5c5fe4829ddb0ba577d85a`, `#10468`, `#10230`, `#10526`, `#9545`, `#10599`

## Fix status

- Merged to main in PR #10599 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-16`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
