hermes - ✅(Solved) Fix [Bug]: Credential pool entries persist after custom_provider removal via `hermes model` — interactive remove menu doesn't show orphaned custom providers [2 pull requests, 4 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#14218Fetched 2026-04-23 07:46:01
View on GitHub
Comments
4
Participants
3
Timeline
10
Reactions
0
Timeline (top)
commented ×4labeled ×4cross-referenced ×2

Error Message

def _get_custom_provider_names() -> list: """Return list of (display_name, pool_key, provider_key) tuples.""" try: from hermes_cli.config import get_compatible_custom_providers, load_config config = load_config() except Exception: config = {}

result = []
# First, add providers from current config.yaml
for entry in get_compatible_custom_providers(config):
    # ... existing logic ...
    result.append((name.strip(), pool_key, provider_key))

# Then, scan auth.json for orphaned custom provider entries
try:
    from agent.credential_pool import load_pool, CUSTOM_POOL_PREFIX
    auth_store = _load_auth_store()
    credential_pool = auth_store.get("credential_pool", {})
    for pool_key in credential_pool.keys():
        if pool_key.startswith(CUSTOM_POOL_PREFIX) and pool_key not in [r[1] for r in result]:
            # Extract display name from pool_key (e.g., "custom:yunwu.ai" → "Yunwu.ai")
            display_name = pool_key.replace(CUSTOM_POOL_PREFIX, "").replace("-", ".").title()
            result.append((display_name, pool_key, ""))
except Exception:
    pass  # Gracefully skip if auth.json unavailable

return result

Root Cause

The issue is in hermes_cli/auth_commands.py:

  1. Line 39-57: _get_custom_provider_names() reads from current config.yaml only
  2. Line 449-463: _pick_provider() displays custom providers from _get_custom_provider_names() — orphaned entries not shown
  3. Line 317-341: auth_list_command() reads directly from auth.json via load_pool() — shows all entries including orphans
  4. Line 322-326: Provider list built from PROVIDER_REGISTRY + list_custom_pool_providers() — but interactive mode uses different logic

The mismatch: auth_list_command() and _interactive_remove() use different sources of truth for custom provider discovery.

Fix Action

Fixed

PR fix notes

PR #14269: fix: show orphaned custom auth pools in interactive remove

Description (problem / solution / changelog)

Summary

  • merge configured custom providers with persisted custom:* auth pools when building interactive provider hints
  • keep configured display names when they still exist, and synthesize orphaned entries from auth.json when config.yaml no longer has them
  • add regression coverage for orphaned custom pool listing and selection in the interactive picker

Testing

  • python3 -m pytest -o addopts='' tests/hermes_cli/test_auth_commands.py

Fixes #14218

Changed files

  • hermes_cli/auth_commands.py (modified, +17/-10)
  • tests/hermes_cli/test_auth_commands.py (modified, +83/-0)

PR #14275: fix(tui): preserve config on save, surface orphaned auth credentials

Description (problem / solution / changelog)

Body

The TUI gateway writes config back to disk with raw yaml.safe_dump(), which strips comments and env-var templates like ${GLM_API_KEY}. Every time the TUI sets a display preference it silently destroys those parts of the user's config. This switches _save_cfg() to delegate to save_config() — the same atomic write path the CLI uses — so env-var templates, comments, and structure survive round-trips.

The second fix addresses orphaned credentials. When a custom provider is removed from config.yaml, its entries stay in auth.json. hermes auth list shows them (reads auth.json) but the interactive remove menu doesn't (reads config.yaml only). _get_custom_provider_names() now also scans auth.json for orphaned custom:* pool keys so they appear in the remove menu. Closes #14218.

Changes

  • tui_gateway/server.py: _save_cfg() delegates to save_config() with HERMES_HOME env var sync
  • hermes_cli/auth_commands.py: _get_custom_provider_names() merges orphaned auth.json entries

Testing

89 tests pass (TUI gateway server + auth commands). Config round-trip verified byte-identical on a 331-line config with custom_providers, mcp_servers, matrix config, and env-var templates.

Changed files

  • hermes_cli/auth_commands.py (modified, +20/-2)
  • tui_gateway/server.py (modified, +15/-5)

Code Example

custom_providers:
   - name: Yunwu.ai
     base_url: https://yunwu.ai/v1
     api_key: sk-xxxxx

---

custom:yunwu.ai (2 credentials):
     #1  Yunwu.ai             api_key config:Yunwu.ai
     #2  model_config         api_key model_config

---

Report: (run `hermes debug share` and paste links)

---

def _get_custom_provider_names() -> list:
    """Return list of (display_name, pool_key, provider_key) tuples."""
    try:
        from hermes_cli.config import get_compatible_custom_providers, load_config
        config = load_config()
    except Exception:
        config = {}
    
    result = []
    # First, add providers from current config.yaml
    for entry in get_compatible_custom_providers(config):
        # ... existing logic ...
        result.append((name.strip(), pool_key, provider_key))
    
    # Then, scan auth.json for orphaned custom provider entries
    try:
        from agent.credential_pool import load_pool, CUSTOM_POOL_PREFIX
        auth_store = _load_auth_store()
        credential_pool = auth_store.get("credential_pool", {})
        for pool_key in credential_pool.keys():
            if pool_key.startswith(CUSTOM_POOL_PREFIX) and pool_key not in [r[1] for r in result]:
                # Extract display name from pool_key (e.g., "custom:yunwu.ai""Yunwu.ai")
                display_name = pool_key.replace(CUSTOM_POOL_PREFIX, "").replace("-", ".").title()
                result.append((display_name, pool_key, ""))
    except Exception:
        pass  # Gracefully skip if auth.json unavailable
    
    return result
RAW_BUFFERClick to expand / collapse

Bug Description

When a custom provider is removed from config.yaml (either manually or via hermes model interactive command), its credential pool entries remain in ~/.hermes/auth.json. However, these orphaned credentials become invisible to the interactive hermes auth remove menu, creating a cleanup gap where users cannot remove them through the official CLI workflow.

The root cause is that _get_custom_provider_names() in auth_commands.py reads from the current config.yaml, while auth.json retains stale entries. This creates a state desynchronization between the configuration source and the runtime credential store.

Steps to Reproduce

  1. Add a custom provider to ~/.hermes/config.yaml:

    custom_providers:
    - name: Yunwu.ai
      base_url: https://yunwu.ai/v1
      api_key: sk-xxxxx
  2. Run hermes auth list — confirms custom:yunwu.ai has 2 credentials (one from config:Yunwu.ai, one from model_config)

  3. Remove the provider via hermes model interactive command (or manually delete from config.yaml)

  4. Run hermes auth list again — orphaned credentials still appear:

    custom:yunwu.ai (2 credentials):
      #1  Yunwu.ai             api_key config:Yunwu.ai
      #2  model_config         api_key model_config
  5. Run hermes auth (interactive mode) → select "Remove a credential" — yunwu does not appear in the provider selection menu

  6. User cannot remove these credentials through the interactive workflow

Expected Behavior

  • When a custom provider is removed from config.yaml, its credential pool entries should either:

    1. Be automatically cleaned up from auth.json on next startup, OR
    2. Still appear in the interactive hermes auth remove menu so users can manually delete them
  • The interactive remove menu should show ALL providers with entries in auth.json, not just those currently in config.yaml

Actual Behavior

  • hermes auth list shows orphaned credentials (reads from auth.json)
  • hermes auth interactive remove menu does NOT show orphaned providers (reads from config.yaml via _get_custom_provider_names())
  • Users must manually edit auth.json or use hermes auth remove custom:yunwu.ai --id <id> with exact provider key syntax

Affected Component

  • CLI (interactive chat)
  • Gateway (Telegram/Discord/Slack/WhatsApp)
  • Setup / Installation
  • Tools (terminal, file ops, web, code execution, etc.)
  • Skills (skill loading, skill hub, skill guard)
  • Agent Core (conversation loop, context compression, memory)
  • Configuration (config.yaml, .env, hermes setup)
  • Other

Debug Report

Report: (run `hermes debug share` and paste links)

Environment

  • Operating System: Ubuntu 24.04
  • Python Version: (run python --version)
  • Hermes Version: (run hermes version)

Root Cause Analysis

The issue is in hermes_cli/auth_commands.py:

  1. Line 39-57: _get_custom_provider_names() reads from current config.yaml only
  2. Line 449-463: _pick_provider() displays custom providers from _get_custom_provider_names() — orphaned entries not shown
  3. Line 317-341: auth_list_command() reads directly from auth.json via load_pool() — shows all entries including orphans
  4. Line 322-326: Provider list built from PROVIDER_REGISTRY + list_custom_pool_providers() — but interactive mode uses different logic

The mismatch: auth_list_command() and _interactive_remove() use different sources of truth for custom provider discovery.

Proposed Fix

Option 1 (Recommended): Modify _get_custom_provider_names() to also scan auth.json for orphaned custom provider entries:

def _get_custom_provider_names() -> list:
    """Return list of (display_name, pool_key, provider_key) tuples."""
    try:
        from hermes_cli.config import get_compatible_custom_providers, load_config
        config = load_config()
    except Exception:
        config = {}
    
    result = []
    # First, add providers from current config.yaml
    for entry in get_compatible_custom_providers(config):
        # ... existing logic ...
        result.append((name.strip(), pool_key, provider_key))
    
    # Then, scan auth.json for orphaned custom provider entries
    try:
        from agent.credential_pool import load_pool, CUSTOM_POOL_PREFIX
        auth_store = _load_auth_store()
        credential_pool = auth_store.get("credential_pool", {})
        for pool_key in credential_pool.keys():
            if pool_key.startswith(CUSTOM_POOL_PREFIX) and pool_key not in [r[1] for r in result]:
                # Extract display name from pool_key (e.g., "custom:yunwu.ai" → "Yunwu.ai")
                display_name = pool_key.replace(CUSTOM_POOL_PREFIX, "").replace("-", ".").title()
                result.append((display_name, pool_key, ""))
    except Exception:
        pass  # Gracefully skip if auth.json unavailable
    
    return result

Option 2: Add a cleanup command hermes auth cleanup that removes all credential pool entries for providers no longer in config.yaml.

PR Willingness

  • I'd like to fix this myself and submit a PR
  • I'm reporting this for the maintainers to fix

extent analysis

TL;DR

Modify the _get_custom_provider_names() function to scan auth.json for orphaned custom provider entries, ensuring the interactive remove menu displays all providers with entries in auth.json.

Guidance

  • Update the _get_custom_provider_names() function to read from both config.yaml and auth.json to ensure all custom providers are listed, including orphaned ones.
  • Verify the fix by running hermes auth list and checking if the orphaned credentials are visible in the interactive remove menu.
  • Consider adding a cleanup command hermes auth cleanup as an alternative solution to remove unused credential pool entries.
  • Test the changes thoroughly to ensure the interactive remove menu works as expected and orphaned credentials can be removed.

Example

The proposed fix provides an example code snippet that modifies the _get_custom_provider_names() function to scan auth.json for orphaned custom provider entries:

def _get_custom_provider_names() -> list:
    # ... existing logic ...
    # Then, scan auth.json for orphaned custom provider entries
    try:
        from agent.credential_pool import load_pool, CUSTOM_POOL_PREFIX
        auth_store = _load_auth_store()
        credential_pool = auth_store.get("credential_pool", {})
        for pool_key in credential_pool.keys():
            if pool_key.startswith(CUSTOM_POOL_PREFIX) and pool_key not in [r[1] for r in result]:
                # Extract display name from pool_key (e.g., "custom:yunwu.ai" → "Yunwu.ai")
                display_name = pool_key.replace(CUSTOM_POOL_PREFIX, "").replace("-", ".").title()
                result.append((display_name, pool_key, ""))
    except Exception:
        pass  # Gracefully skip if auth.json unavailable

Notes

The proposed fix assumes that the auth.json file is properly formatted and accessible. If the file is corrupted or inaccessible, the fix may not work as expected.

Recommendation

Apply the workaround by modifying the _get_custom_provider_names() function to scan auth.json for orphaned custom provider entries, as this solution directly addresses the root cause of the issue and provides a straightforward fix.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug]: Credential pool entries persist after custom_provider removal via `hermes model` — interactive remove menu doesn't show orphaned custom providers [2 pull requests, 4 comments, 3 participants]