hermes - ✅(Solved) Fix Profile creation accepts reserved names like 'main' (re-introduces #12099 in a corner case) [1 pull requests, 1 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#17879Fetched 2026-05-01 05:55:20
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×3cross-referenced ×1

Root Cause

hermes_cli/profiles.py defines _RESERVED_NAMES = {"hermes", "default", "test", "tmp", "root", "sudo"} (line 126), but:

  1. validate_profile_name() at line 182 only checks the regex _PROFILE_ID_RE; it does not consult _RESERVED_NAMES at all.
  2. create_profile() at line 395 hardcodes a single if name == "default" rejection but does not check _RESERVED_NAMES either.
  3. The _RESERVED_NAMES frozenset is only consumed by check_alias_collision() (line 211), which runs when creating wrapper scripts under ~/.local/bin/<name> — too late and orthogonal to profile creation.

So every reserved name except default slips through silently, and "main" was never in the list to begin with even though it would re-create the original bug.

Fix Action

Fixed

PR fix notes

PR #17880: fix(profiles): reserve "main" + enforce _RESERVED_NAMES in create_profile

Description (problem / solution / changelog)

Fixes #17879.

TL;DR

hermes profile create main succeeded and produced session keys with the agent:main:* prefix — the same prefix the default profile uses — silently re-creating the cross-profile collision that #12099 / the in-flight #12266 fix exists to eliminate. Same hole let root, hermes, test, tmp, sudo through. _RESERVED_NAMES was defined in hermes_cli/profiles.py but only consulted by check_alias_collision() (wrapper-script creation), not by profile creation itself.

This PR adds "main" to _RESERVED_NAMES and makes create_profile() reject any reserved name with a uniform message.

Why this is independent of #12266

I noticed this while reviewing #12266 (which is excellent and which I think should land — see #12102 close note). #12266 fixes the runtime behaviour of session-key construction so two profiles with different names don't collide. This PR fixes the input-validation layer so a user can't ask the runtime to construct a "main"-named profile in the first place. The two fixes are orthogonal and can land in either order:

  • After #12266 alone: hermes profile create main_resolve_session_key_prefix("main")"agent:main" → collision with default still happens.
  • After this PR alone: collision avoided for new profiles, but already-fixed-by-#12266 cases (named profiles) still need #12266's work to disambiguate.
  • Together: both vectors closed.

Diff

hermes_cli/profiles.py            | +9 / -4
tests/hermes_cli/test_profiles.py | +53 / -0

Net 5 lines of production change.

Behavior matrix

CommandBefore this PRAfter this PR
hermes profile create coder✓ created✓ created (unchanged)
hermes profile create default✗ "Cannot create… built-in"✗ "…reserved. Reserved names: …"
hermes profile create main✓ created (bug)✗ "…reserved"
hermes profile create root✓ created (bug)✗ "…reserved"
hermes profile create hermes / test / tmp / sudo✓ created (bug)✗ "…reserved"
rename_profile, set_active_profile, etc. on a legacy "main" profileworksworks (validate_profile_name unchanged)

Scope — explicitly NOT changed

  • validate_profile_name() stays permissive. It's called by 8 non-creation paths (rename, delete, set_active, export, import, resolve_env, …). Tightening it would break any user who somehow already has a legacy "main" profile on disk — they should still be able to rename or delete it. Only creating a new reserved-name profile is what we reject.
  • No change to _resolve_session_key_prefix or build_session_key. That layer is #12266's scope and this PR doesn't touch it.
  • No migration of existing "main" profiles. Out of scope; a follow-up could detect and warn at startup, but the realistic exposure is near-zero (you'd have had to deliberately type hermes profile create main, which was undocumented).

Tests

tests/hermes_cli/test_profiles.py::TestReservedNames adds 9 cases:

  • Parametrised across all 7 reserved names (main, hermes, default, test, tmp, root, sudo) — each must raise ValueError with match="reserved".
  • Error message must name the offending value AND list hermes, default, main so the user can correct without reading source.
  • Reserved set drift canary: if someone later changes _RESERVED_NAMES, this test fails and forces an intentional update.
  • Bug-shaped check: failed creation must not leave an orphan directory.

All 102 existing tests/hermes_cli/test_profiles.py cases still pass on this branch (scripts/run_tests.sh tests/hermes_cli/test_profiles.py). The legacy test_default_raises_value_error continues to pass because the new error message still contains the literal string "default".

Pre-empted review questions

Q. Why is the new error message verbose?
The reserved set is small (7 names) and listing them is a lot cheaper than making the user grep profiles.py. If you'd rather a terser message, happy to drop the list.

Q. Why parametrise _RESERVED_NAMES in tests instead of hardcoding ["main"]?
The bug is that the frozenset existed but wasn't enforced. Parametrising every name pins all seven so a future contributor who adds a new reserved name automatically gets a passing test, and one who removes the enforcement gets a failing one.

Q. Should "default" be removed from _RESERVED_NAMES since validate_profile_name already special-cases it?
No — keeping "default" in _RESERVED_NAMES makes the creation-side rejection uniform (one code path, one message). The validate_profile_name special case is for the non-creation paths (rename, delete, etc.) where "default" is a valid alias for the built-in profile.

Changed files

  • hermes_cli/profiles.py (modified, +9/-4)
  • tests/hermes_cli/test_profiles.py (modified, +53/-0)

Code Example

$ hermes profile create main
Created profile 'main' at ~/.hermes/profiles/main
$ HERMES_HOME=~/.hermes/profiles/main hermes gateway start
# inbound Telegram messages now build session keys like
#   agent:main:telegram:dm:<chat_id>
# colliding with the default profile's namespace in any external memory
# backend (Honcho / RetainDB / ByteRover) that consumes the gateway key
# verbatim — see plugins/memory/honcho/client.py::resolve_session_name.
RAW_BUFFERClick to expand / collapse

Bug

hermes profile create main succeeds today. So do hermes profile create root, hermes profile create hermes, hermes profile create test, hermes profile create tmp, hermes profile create sudo — every name in _RESERVED_NAMES except default.

The "main" case is the most consequential: a profile named main produces session keys with the agent:main:* prefix, which is the exact prefix the default profile uses. This re-introduces the cross-profile session-key collision that #12099 / the in-flight #12266 fix was meant to eliminate.

Repro

$ hermes profile create main
✓ Created profile 'main' at ~/.hermes/profiles/main
$ HERMES_HOME=~/.hermes/profiles/main hermes gateway start
# inbound Telegram messages now build session keys like
#   agent:main:telegram:dm:<chat_id>
# colliding with the default profile's namespace in any external memory
# backend (Honcho / RetainDB / ByteRover) that consumes the gateway key
# verbatim — see plugins/memory/honcho/client.py::resolve_session_name.

Root cause

hermes_cli/profiles.py defines _RESERVED_NAMES = {"hermes", "default", "test", "tmp", "root", "sudo"} (line 126), but:

  1. validate_profile_name() at line 182 only checks the regex _PROFILE_ID_RE; it does not consult _RESERVED_NAMES at all.
  2. create_profile() at line 395 hardcodes a single if name == "default" rejection but does not check _RESERVED_NAMES either.
  3. The _RESERVED_NAMES frozenset is only consumed by check_alias_collision() (line 211), which runs when creating wrapper scripts under ~/.local/bin/<name> — too late and orthogonal to profile creation.

So every reserved name except default slips through silently, and "main" was never in the list to begin with even though it would re-create the original bug.

Scope

  • This issue is orthogonal to #12099 / #12266 — even after #12266 lands, a user who runs hermes profile create main still hits the original collision because _resolve_session_key_prefix("main") evaluates to "agent:main".
  • Reserving "main" as a profile name closes that gap permanently.

Proposed fix

  • Add "main" to _RESERVED_NAMES.
  • Have create_profile() reject any name in _RESERVED_NAMES (replacing the one-name "default" hardcode with a uniform check).
  • Keep validate_profile_name() permissive so existing callers (rename_profile, delete_profile, set_active_profile, export_profile, import_profile) still work for any legacy "main" profile that may already exist on disk — only creation is restricted.

PR coming shortly.

extent analysis

TL;DR

To fix the issue, add "main" to the _RESERVED_NAMES set and modify the create_profile() function to reject any name in _RESERVED_NAMES.

Guidance

  • Identify the _RESERVED_NAMES set in hermes_cli/profiles.py and add "main" to it to prevent profile name collisions.
  • Update the create_profile() function to check if the provided profile name is in the _RESERVED_NAMES set and reject it if so, rather than just hardcoding a check for "default".
  • Ensure that the validate_profile_name() function remains permissive to allow existing callers to work with legacy "main" profiles.
  • Verify the fix by attempting to create a profile named "main" and confirming that it is rejected.

Example

_RESERVED_NAMES = {"hermes", "default", "test", "tmp", "root", "sudo", "main"}

def create_profile(name):
    if name in _RESERVED_NAMES:
        # Reject the profile creation
        raise ValueError(f"Profile name '{name}' is reserved")
    # Rest of the function remains the same

Notes

This fix assumes that the goal is to prevent profile name collisions and ensure that the default profile's namespace is not overwritten. The proposed fix only restricts profile creation and does not affect existing profiles.

Recommendation

Apply the workaround by adding "main" to _RESERVED_NAMES and modifying the create_profile() function to reject reserved names, as this will prevent profile name collisions and ensure the stability of the default profile's namespace.

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 Profile creation accepts reserved names like 'main' (re-introduces #12099 in a corner case) [1 pull requests, 1 participants]