# NousResearch/hermes-agent · Profile config sync and migration during updates

## Open now

- Nothing open on GitHub.

These issues and PRs concern Hermes update and startup commands not consistently syncing bundled skills or migrating config across all profiles, with fixes targeting both the update flow and serve/dashboard startup.

## TL;DR

- 6 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #19869. Verify it addresses your case.
- **Recommended:** read PR #19869 (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_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.31`: 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

- #16176: \`hermes update\` skips bundled skill sync for the active profile
- #20438: Config migration is profile-local; non-active profiles silently drift across Hermes upgrades
- #54926: hermes update should migrate all profiles and clean up deprecated toolsets from config

## Prior & competing PRs

- **#19869: fix(update): sync bundled skills to all profiles, including active**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `tests/hermes_cli/test_cmd_update.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -7069,20 +7069,22 @@ def _cmd_update_impl(args, gateway_mode: bool):
             except Exception as e:
                 logger.debug("Skills sync during update failed: %s", e)
     
    -        # Sync bundled skills to all other profiles
    +        # Sync bundled skills to all profiles (including the active one).
    +        # seed_profile_skills() uses subprocess with an explicit HERMES_HOME so
    +        # it is not affected by sync_skills()'s module-level HERMES_HOME cache,
    +        # which means the active profile is reliably synced regardless of whether
    +        # the caller's HERMES_HOME env var points at the default or a named profile.
             try:
                 from hermes_cli.profiles import (
                     list_profiles,
    -                get_active_profile_name,
                     seed_profile_skills,
                 )
     
    -            active = get_active_profile_name()
    -            other_profiles = [p for p in list_profiles() if p.name != active]
    -            if other_profiles:
    +            all_profiles = list_profiles()
    +            if all_profiles:
                     print()
    -                print("→ Syncing bundled skills to other profiles...")
    -                for p in other_profiles:
    +                print("→ Syncing bundled skills to all profiles...")
    +                for p in all_profiles:
                         try:
                             r = seed_profile_skills(p.path, quiet=True)
                             if r:
    ```
  - `tests/hermes_cli/test_cmd_update.py`

    ```diff
    @@ -163,3 +163,78 @@ def test_update_non_interactive_skips_migration_prompt(self, mock_args, capsys):
                 mock_input.assert_not_called()
                 captured = capsys.readouterr()
                 assert "Non-interactive session" in captured.out
    +
    +
    +class TestCmdUpdateProfileSkillSync:
    +    """cmd_update syncs bundled skills to all profiles, including the active one.
    +
    +    Regression guard for #16176: previously the active profile was excluded
    +    from the seed_profile_skills loop, leaving it on stale skill content.
    +    """
    +
    +    @patch("shutil.which", return_value=None)
    +    @patch("subprocess.run")
    +    def test_active_profile_included_in_skill_sync(
    +        self, mock_run, _mock_which, mock_args, capsys
    +    ):
    +        from pathlib import Path
    +
    +        mock_run.side_effect = _make_run_side_effect(
    +            branch="main", verify_ok=True, commit_count="1"
    +        )
    +
    +        default_p = SimpleNamespace(name="default", path=Path("/fake/.hermes"))
    +        active_p = SimpleNamespace(name="bit", path=Path("/fake/.hermes/profiles/bit"))
    +        other_p = SimpleNamespace(name="work", path=Path("/fake/.hermes/profiles/work"))
    +        all_profiles = [default_p, active_p, other_p]
    +
    +        synced_paths = []
    +
    +        def fake_seed(path, quiet=False):
    +            synced_paths.append(path)
    +            return {"copied": [], "updated": [], "user_modified": []}
    +
    +        empty_sync = {"copied": [], "updated": [], "user_modified": [], "cleaned": []}
    +
    +        with (
    +            patch("hermes_cli.profiles.list_profiles", return_value=all_profiles),
    +            patch("hermes_cli.profiles.seed_profile_skills", side_effect=fake_seed),
    +            patch("tools.skills_sync.sync_skills", return_value=empty_sync),
    +        ):
    +            cmd_update(mock_args)
    +
    +        assert active_p.path in synced_paths, (
    +            f"Active profile 'bit' must be included in skill sync; got: {synced_paths}"
    +        )
    +        assert set(synced_paths) == {p.path for p in all_profiles}, (
    +            f"All profiles must be synced; got: {synced_paths}"
    +        )
    +
    +    @patch("shutil.which", return_value=None)
    +    @patch("subprocess.run")
    +    def test_single_profile_default_is_synced(
    +        self, mock_run, _mock_which, mock_args, capsys
    +    ):
    +        from pathlib import Path
    +
    +        mock_run.side_effect = _make_run_side_effect(
    +            branch="main", verify_ok=True, commit_count="1"
    +        )
    +
    +        default_p = SimpleNamespace(name="default", path=Path("/fake/.hermes"))
    +        synced_paths = []
    +
    +        def fake_seed(path, quiet=False):
    +            synced_paths.append(path)
    +            return {"copied": [], "updated": [], "user_modified": []}
    +
    +        empty_sync = {"copied": [], "updated": [], "user_modified": [], "cleaned": []}
    +
    +        with (
    +            patch("hermes_cli.profiles.list_profiles", return_value=[default_p]),
    +            patch("hermes_cli.profiles.seed_profile_skills", side_effect=fake_seed),
    +            patch("tools.skills_sync.sync_skills", return_value=empty_sync),
    +        ):
    +            cmd_update(mock_args)
    +
    +        assert default_p.path in synced_paths
    ```
- **#16217: fix(update): sync bundled skills to all profiles, including active (#16176)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#19869`
- **#62492: fix(config): auto-migrate stale profile config on serve/dashboard startup**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#91277`

## Fix status

- Merged to main in PR #19869 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-04`.
- Subsystem: `hermes_cli`
