Skip to content

fix(hermesagent): follow the platform default profile root and stabilize the shared-file key - #2483

Merged
dyoshikawa merged 3 commits into
mainfrom
resolve-scrap-issue-2439-hermes-home-refactor
Jul 29, 2026
Merged

fix(hermesagent): follow the platform default profile root and stabilize the shared-file key#2483
dyoshikawa merged 3 commits into
mainfrom
resolve-scrap-issue-2439-hermes-home-refactor

Conversation

@dyoshikawa

Copy link
Copy Markdown
Owner

Part of #2439 (findings 1, 2, 5, 9, 10) — the refactor bundle the maintainer asked for, with finding 5 falling out of it. Findings 3, 4, 6, 7, 8 follow in a separate PR.

Finding 1 (mid) — Windows default profile root

Upstream _get_platform_default_hermes_home() returns %LOCALAPPDATA%\hermes on win32 and ~/.hermes only elsewhere, but getHermesagentHome() fell back to ~/.hermes on every platform, so a global generate on native Windows with HERMES_HOME unset wrote where Hermes never reads.

getHermesagentRelativeDirPath now strips the canonical .hermes prefix and re-anchors it: nothing is prepended when HERMES_HOME is set (it is the profile root itself), otherwise the platform default from the new getHermesagentGlobalDir() takes its place. The path constants stay canonical, so this is one funnel rather than a de-prefixing of every constant. docs/reference/supported-tools.md loses the sentence claiming "the default remains ~/.hermes".

Finding 2 (mid) — env-dependent shared-file key

sharedFileKey() collapsed to a bare config.yaml whenever HERMES_HOME was set, while writers passed the constant .hermes/config.yaml — so the ownership lock-step guard and the shared-file contract test went blind in exactly the configuration this feature is about (reproducible before this PR with HERMES_HOME=/tmp/x npx vitest run src/features/shared/shared-config-gateway.test.ts).

Rather than make the writers' key env-dependent too, all five Hermes writers of config.yaml (commands, subagents, mcp, hooks, permissions) now declare every spelling the profile root can take through getExtraSharedWritePaths.hermes/, AppData/Local/hermes/, and the root itself — so the derived key set is identical on every platform and with or without HERMES_HOME. SHARED_CONFIG_OWNERSHIP declares all three with the same policies. This is the shape #2450 used for the two Zed global paths.

Finding 9 (low) — env-derived outputRoot bypassed validation

resolveToolOutputRoot now runs validateOutputRoot on a substituted home override, so HERMES_HOME=/ is rejected instead of becoming the output root. Closes the same pre-existing hole for KIMI_CODE_HOME.

Finding 10 (low) — weak de-prefix guard

The guard rejected only an exact .., a ..-prefix, and absolute paths. It now uses checkPathTraversal on both the input and the de-prefixed result — checking both matters because relative() normalizes .. away, so a path that walks out of .hermes and back in would pass a check on the result alone.

Finding 5 (low) — hermetic tests, resolved as a side effect

HERMES_HOME and KIMI_CODE_HOME are cleared in vitest.config.ts before any test module loads. They have to be neutralized at that level rather than in a beforeEach, because the shared-write derivation runs at module load. Verified: HERMES_HOME=/tmp/x KIMI_CODE_HOME=/tmp/y npx vitest run over the previously-affected specs (hermesagent-mcp, hermesagent-skill, hermesagent-subagent, shared-config-gateway, shared-file-contract, utils/hermesagent) is green, where it produced 12 failures before.

Tests

New unit coverage for the platform default, the traversal guard, and the env-independence of the declared write paths. E2E specs now derive Hermes global paths from getHermesagentGlobalDir() instead of hardcoding .hermes, which is what makes the win32 default observable on the windows-latest E2E run. Full pnpm cicheck is green, as are every Hermes-touching e2e spec locally (mcp, hooks, permissions, commands, subagents, skills, checks, ignore, hermesagent-home).

🤖 Generated with Claude Code

cm-dyoshikawa and others added 3 commits July 29, 2026 09:23
…ize the shared-file key

Four follow-ups from the #2394 review (issue #2439, findings 1, 2, 9, 10):

- Upstream Hermes defaults its profile root to %LOCALAPPDATA%\hermes on win32
  and ~/.hermes elsewhere, but rulesync wrote ~/.hermes on every platform when
  HERMES_HOME was unset, so a global generate on Windows landed where Hermes
  never reads. getHermesagentRelativeDirPath now re-anchors the de-prefixed
  path on the platform default, and the false docs sentence is corrected.
- The shared-file key derived from getSettablePaths depended on HERMES_HOME
  (and now the platform), so the gateway drift guards went blind in exactly
  the configuration this feature targets. Every Hermes config.yaml writer now
  declares all three spellings through getExtraSharedWritePaths, and the
  ownership table declares them alongside .hermes/config.yaml.
- resolveToolOutputRoot validates a substituted home override with
  validateOutputRoot, closing the same hole for KIMI_CODE_HOME.
- The de-prefix guard uses checkPathTraversal semantics on both the input and
  the de-prefixed result, rejecting any .. segment rather than a bare prefix.

The unit suite is now hermetic with respect to HERMES_HOME and KIMI_CODE_HOME
(finding 5): both are cleared in vitest.config.ts before any module loads,
since they change paths resolved at module load. E2E specs use the
platform-aware directory instead of a hardcoded .hermes for global scope.

Part of #2439 (findings 1, 2, 5, 9, 10).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the path written

Review follow-ups on PR #2483:

- The three Hermes config.yaml ownership entries (and now the two kimi-code
  config.toml entries) share one hoisted declaration, so a policy edit cannot
  land on a single spelling while the others keep the old semantics.
- Writers pass the key of the file they are actually writing via
  getHermesagentConfigSharedFileKey / getKimiCodeConfigSharedFileKey instead of
  a constant that only names the non-HERMES_HOME, non-win32 spelling. This is
  the sharedConfigFileKey convention the Zed and Codex adapters already follow,
  and it makes the new declarations live rather than guard-only data.
- kimi-code gets the same two-spelling treatment: KIMI_CODE_HOME made its
  derived key collapse to a bare config.toml, so both drift guards broke for it
  exactly as they did for hermes. Fixing it here keeps the vitest env clearing
  from masking a live bug.
- resolveToolOutputRoot re-throws a rejected home override naming the variable
  (the user never passed --output-roots), and gains a unit test file.
- vitest.e2e.config.ts clears the same two overrides; e2e helpers spread
  process.env into the CLI child, so an exported one would redirect global
  output away from the pseudo-home the specs assert against.
- Records why the win32 profile dir is home-relative rather than read from
  LOCALAPPDATA, matching the Zed and Warp constants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lper signature

Review round two (all low):

- getKimiCodeConfigSharedFileKey takes { global } like its hermes counterpart
  instead of hardcoding true, so the write-time key cannot drift from the
  settable path the day kimi-code gains a project scope.
- Direct unit coverage for both key helpers: they exist precisely because the
  answer depends on the home override and the platform, and the previous
  protection was indirect (the gateway throwing on an undeclared key in e2e).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dyoshikawa
dyoshikawa merged commit 2671916 into main Jul 29, 2026
9 checks passed
@dyoshikawa
dyoshikawa deleted the resolve-scrap-issue-2439-hermes-home-refactor branch July 29, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants