fix: gate Claude Code credentials behind explicit Hermes config in wizard trigger (#4210)

If a user has Claude Code installed but never configured Hermes, the
first-run guard found those external credentials and skipped the setup
wizard. Users got silently routed to someone else's inference without
being asked.

Now _has_any_provider_configured() checks whether Hermes itself has been
explicitly configured (model in config differs from hardcoded default)
before counting Claude Code credentials. Fresh installs trigger the
wizard regardless of what external tools are on the machine.

Salvaged from PR #4194 by sudoingX — wizard trigger fix only.
Model auto-detect change under separate review.

Co-authored-by: Xpress AI (Dip KD) <200180104+sudoingX@users.noreply.github.com>
This commit is contained in:
Teknium
2026-03-31 02:01:15 -07:00
committed by GitHub
parent c53a296df1
commit 086ec5590d
2 changed files with 78 additions and 10 deletions

View File

@@ -173,9 +173,25 @@ def _relative_time(ts) -> str:
def _has_any_provider_configured() -> bool:
"""Check if at least one inference provider is usable."""
from hermes_cli.config import get_env_path, get_hermes_home
from hermes_cli.config import get_env_path, get_hermes_home, load_config
from hermes_cli.auth import get_auth_status
# Determine whether Hermes itself has been explicitly configured (model
# in config that isn't the hardcoded default). Used below to gate external
# tool credentials (Claude Code, Codex CLI) that shouldn't silently skip
# the setup wizard on a fresh install.
from hermes_cli.config import DEFAULT_CONFIG
_DEFAULT_MODEL = DEFAULT_CONFIG.get("model", "")
cfg = load_config()
model_cfg = cfg.get("model")
if isinstance(model_cfg, dict):
_model_name = (model_cfg.get("default") or "").strip()
elif isinstance(model_cfg, str):
_model_name = model_cfg.strip()
else:
_model_name = ""
_has_hermes_config = _model_name and _model_name != _DEFAULT_MODEL
# Check env vars (may be set by .env or shell).
# OPENAI_BASE_URL alone counts — local models (vLLM, llama.cpp, etc.)
# often don't require an API key.
@@ -231,15 +247,16 @@ def _has_any_provider_configured() -> bool:
# Check for Claude Code OAuth credentials (~/.claude/.credentials.json)
# These are used by resolve_anthropic_token() at runtime but were missing
# from this startup gate check.
try:
from agent.anthropic_adapter import read_claude_code_credentials, is_claude_code_token_valid
creds = read_claude_code_credentials()
if creds and (is_claude_code_token_valid(creds) or creds.get("refreshToken")):
return True
except Exception:
pass
# Only count these if Hermes has been explicitly configured — Claude Code
# being installed doesn't mean the user wants Hermes to use their tokens.
if _has_hermes_config:
try:
from agent.anthropic_adapter import read_claude_code_credentials, is_claude_code_token_valid
creds = read_claude_code_credentials()
if creds and (is_claude_code_token_valid(creds) or creds.get("refreshToken")):
return True
except Exception:
pass
return False

View File

@@ -622,6 +622,57 @@ class TestHasAnyProviderConfigured:
from hermes_cli.main import _has_any_provider_configured
assert _has_any_provider_configured() is True
def test_claude_code_creds_ignored_on_fresh_install(self, monkeypatch, tmp_path):
"""Claude Code credentials should NOT skip the wizard when Hermes is unconfigured."""
from hermes_cli import config as config_module
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setattr(config_module, "get_env_path", lambda: hermes_home / ".env")
monkeypatch.setattr(config_module, "get_hermes_home", lambda: hermes_home)
# Clear all provider env vars so earlier checks don't short-circuit
for var in ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN", "OPENAI_BASE_URL"):
monkeypatch.delenv(var, raising=False)
# Simulate valid Claude Code credentials
monkeypatch.setattr(
"agent.anthropic_adapter.read_claude_code_credentials",
lambda: {"accessToken": "sk-ant-test", "refreshToken": "ref-tok"},
)
monkeypatch.setattr(
"agent.anthropic_adapter.is_claude_code_token_valid",
lambda creds: True,
)
from hermes_cli.main import _has_any_provider_configured
assert _has_any_provider_configured() is False
def test_claude_code_creds_counted_when_hermes_configured(self, monkeypatch, tmp_path):
"""Claude Code credentials should count when Hermes has been explicitly configured."""
import yaml
from hermes_cli import config as config_module
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
# Write a config with a non-default model to simulate explicit configuration
config_file = hermes_home / "config.yaml"
config_file.write_text(yaml.dump({"model": {"default": "my-local-model"}}))
monkeypatch.setattr(config_module, "get_env_path", lambda: hermes_home / ".env")
monkeypatch.setattr(config_module, "get_hermes_home", lambda: hermes_home)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Clear all provider env vars
for var in ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN", "OPENAI_BASE_URL"):
monkeypatch.delenv(var, raising=False)
# Simulate valid Claude Code credentials
monkeypatch.setattr(
"agent.anthropic_adapter.read_claude_code_credentials",
lambda: {"accessToken": "sk-ant-test", "refreshToken": "ref-tok"},
)
monkeypatch.setattr(
"agent.anthropic_adapter.is_claude_code_token_valid",
lambda creds: True,
)
from hermes_cli.main import _has_any_provider_configured
assert _has_any_provider_configured() is True
# =============================================================================
# Kimi Code auto-detection tests