Skip to content

install (v0.10.8, Windows): Hermes pre_llm_hook_install refuses config when a plain scalar contains > / | anywhere mid-value; the same value double-quoted installs fine — bisected to two persona strings #1924

Description

@ukind

Filed from a debugged end-user install; supersets/continues #1631 and #1656 (both closed as completed, but the underlying refusal is still reachable in v0.10.8 — this is a new failing construct, not a re-report of the four fixed in v0.10.6).

Summary

  • codebase-memory-mcp install on Windows consistently fails for the Hermes agent with op=pre_llm_hook_install against %LOCALAPPDATA%\hermes\config.yaml. Because agent-config failures abort activation, the whole install exits 1 and user-PATH registration is skipped even though 11 other detected agents configure fine.
  • Deterministic, config-content-dependent: key-by-key and persona-by-persona bisection reduced the trigger to how a scalar is serialized, then to a single character class. A plain (unquoted) scalar that contains > anywhere after the value start is refused; the identical string emitted double-quoted is accepted. Plain | mid-value behaves the same. Plain * mid-value is accepted (consistent with the * fix in v0.10.4).
  • This matches the indicator-scoping rule you already adopted — "quotes and anchors are indicators only where a value begins, the same rule the * fix used in v0.10.4" (v0.10.6 notes) — but apparently that scoping was not extended to > and |.
  • The local config is repaired and install/update now completes end-to-end (exit 0, idempotent). Details and a confirmed minimal A/B/C repro below.

Environment

  • codebase-memory-mcp 0.10.8, amd64 binary 296,140,288 bytes, daemon cohort build b4b403b1d7c4def3785f148b93f345ce8427858f4f5489ce28580c4387a336a6
  • Windows 11 Pro x64 (Windows PowerShell 5.1.26100.9168 present; pwsh 7.6.5)
  • Install channel: documented one-liner (install.ps1 from raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.ps1)
  • Detected agents (13): Claude-Code, Zed, OpenCode, KiloCode, VS-Code, OpenClaw, Hermes, Warp, Copilot-CLI, Factory-Droid, Goose, GitLab Duo CLI, Pi
  • Hermes config at failure: ~4.8 KB, 22 top-level keys, valid per PyYAML (yaml.safe_load round-trip OK), owned by the invoking user, no BOM
  • Redacted paths below: C:\Users\<user>\..., hostname <hostname>

Symptom

error: agent_config agent=Hermes op=pre_llm_hook_install path=C:\Users\<user>\AppData\Local\hermes/config.yaml (target: regular file, 4952 bytes)
error: one or more agent configurations failed; the published/current executable was kept, and PATH/index cleanup was not attempted
error: activation stopped after one or more agent configuration or cleanup operations failed; the published/current executable was kept, and configuration changes that completed may remain. Please restart your coding-agent sessions after resolving the errors above.
...
error: installation failed (exit code 1)

On the very first (virgin) run the mcp_servers: write into the same file succeeded (present on disk thereafter); from then on only pre_llm_hook_install refused — consistent with mcp_install no-op'ing on its existing entry while the hook upsert re-validated the file. (In #1656 on 0.10.5 both ops failed; on 0.10.8 only the hook op fails for our file.)

PR #1629's (target: regular file, N bytes) descriptor is what made this tractable — it correctly signaled a structural refusal on a writable regular file, not permissions or a missing file. Thank you. What remains missing is which structural rule refused (the single--1 collapse noted in #1629 is exactly what turned this into a multi-hour key-level bisect).

Confirmed minimal A/B/C reproduction (v0.10.8, Windows)

Each variant: write only this config to %LOCALAPPDATA%\hermes\config.yaml, run codebase-memory-mcp install -y --force --dir=...:

# A — PASS (exit 0): plain scalar, mid-value `*`, no `>`  → v0.10.4 `*` fix confirmed working
agent:
  personalities:
    probe: hewwo *nuzzles your code* nya

# B — FAIL (exit 1, op=pre_llm_hook_install): plain scalar, mid-value `>`
agent:
  personalities:
    probe: kaomoji face >w< here

# C — PASS (exit 0): the identical string double-quoted
agent:
  personalities:
    probe: "kaomoji face >w< here"

# D — FAIL (exit 1, op=pre_llm_hook_install): plain scalar, mid-value `|`
agent:
  personalities:
    probe: pipe char | mid value

Battery results (each variant alone, same machine, same binary):

variant mid-value char style result
A * plain PASS (exit 0)
B > plain FAILop=pre_llm_hook_install
C > double-quoted PASS (exit 0)
D | plain FAILop=pre_llm_hook_install

Root-cause isolation (how B was reached from the real config)

Key-level bisect of the 4.8 KB Hermes config (all runs with the file LF-normalized, no BOM, original data otherwise byte-equal after a PyYAML round-trip):

step config subset result
1 all 22 original keys + canonical mcp_servers/hooks FAIL
2 keys 0–11 (modelmemory, incl. toolsets: [- all] block seq at col 0) PASS
3 keys 11–22 (session_reset, skills, agent, platform_toolsets, …) FAIL
4 skills alone PASS
5 agent alone FAIL
6 agent w/ only scalar fields (max_turns, verbose, reasoning_effort) + one persona at a time per-persona split below
7 personas one at a time: helpful ✅ · kawaii ❌ · catgirl ✅ · pirate ✅ (single-quoted '' doubling) · shakespeare ✅ · surfer ✅ (apostrophe in double quotes) · noir ✅ · uwu ❌ · hype ✅ (emoji + LET'S) only kawaii, uwu refuse
8 discriminant: the two refusing values are exactly the two containing > mid-value ((>ω<), >w<); no other persona contains > anywhere
9 full original config re-emitted with those two values double-quoted (every other value byte-equivalent) PASS, hook installed, exit 0, idempotent re-run exit 0

Serialization detail (PyYAML allow_unicode=True, width=100000): the refusing variant emitted kawaii/uwu as plain scalars with > after value start. PyYAML itself accepts this — it is valid YAML — indicator characters (and quotes, and *) are only significant at value start, so a mid-scalar > is ordinary content. The refusal is cbm's scanner being stricter than the YAML spec here, which is presumably deliberate fail-closed behavior by design; the problem is it fails closed + silent + install-aborting rather than reporting the construct.

Ruled out during triage (each with evidence)

hypothesis test verdict
content-independence (per #1656) minimal model: glm-4.7 LF/no-BOM → PASS failure is content-dependent; in passing: Set-Content -Encoding UTF8 repros (ours and #1656's 26-byte one) carry a UTF-8 BOM + CRLF, which are extra variables — worth re-running #1656's repro with printf-written LF content
UTF-8 BOM original has none (xxd); BOM-free fixtures both pass and fail depending on > placement not the cause (BOM may be its own refusal — untested in isolation on 0.10.8)
CRLF line endings 130-line CRLF→LF conversion, size 4952→4822 still fails — not the cause
cross-account DACL grant (the acl-grants-cross-account-mutation family install.ps1 documents for its staging dir) explicit non-inherited BUILTIN\Administrators:(F) found on config.yaml; removed via RemoveAccessRuleSpecific+Set-Acl → DACL then identical in shape to agents that pass (only inherited SYSTEM/Administrators/) still fails — not the cause (for the hook op)
directory/file ownership hermes dir owned by BUILTIN\Administrators (created by an elevated Hermes install); took ownership of dir + children to the invoking user still fails — not the cause
mixed path separators (#1656's lead) ran install with LOCALAPPDATA=C:/Users/<user>/AppData/Local (forward slashes) error path unchanged — cbm normalizes internally; the path resolves fine anyway (regular file, N bytes) → not the cause
file lock / read-only exclusive-open test OK; no Hermes processes running (last activity months old) not the cause
malformed YAML PyYAML safe_load OK; 130 lines; no anchors, no multi-doc, no tabs, no control bytes (iconv strict pass) ordinary YAML
foreign pre_llm_call entry error op is pre_llm_hook_install, not pre_llm_hook_foreign; grep confirms no hooks:/pre_llm_call in the file pre-write not the foreign-entry path

Impact

  1. One optional agent's config edit fails closed → entire activation aborts: PATH registration and index cleanup skipped for the user even though 11 other agents were configured fine (re-raises install: Hermes agent_config fails on Windows (content-independent), aborting PATH registration #1656 suggestion 3 — consider isolating per-agent failures from activation completion).
  2. The refusal reason is collapsed into a single exit path (PR fix(cli): say what the agent-config target IS when a write is refused #1629 acknowledges: "the editors should carry their own refusal reason out"). The (target: regular file, N bytes) descriptor was essential and made the bisect possible — a reason string ("unsupported indicator in scalar" + offset) would collapse the remaining space to minutes.

Workaround applied locally (works, data-preserving)

Re-serialized config.yaml with PyYAML using a representer that forces double-quoted style for any string containing >, | (and defensively backtick), LF endings, single-line scalars. All original key/value data preserved (verified parsed-data equal to the original). Hermes reads it fine; install now exits 0 and the canonical hook lands:

hooks:
  pre_llm_call:
    - id: "codebase-memory-mcp"
      type: "command"
      command: "'C:/Users/<user>/AppData/Local/Programs/codebase-memory-mcp/codebase-memory-mcp.exe' hook-augment --dialect hermes"

Repeated install -y --force runs are idempotent (exit 0, hook: recognized, no re-mutation chatter).

Suggested fix / regression tests

  1. Scanner: extend the v0.10.4/v0.10.6 indicator-scoping rule ("indicators only where a value begins") to > and | — a mid-scalar >/| must not flip the scanner into block-scalar/refuse state. (Plain * mid-value is already correctly accepted per the battery above.)
  2. Editor: carry a refusal reason (op + rule that refused) out of the YAML editor, per the postscript in PR fix(cli): say what the agent-config target IS when a write is refused #1629 — that is what would have collapsed this bisect.
  3. Regression fixtures for the CLI suite: plain > mid-value (refused today), plain | mid-value (refused today), double-quoted > (passes today — keep passing), single-quoted '' doubling (passes today), * mid-value (passes today — guards the 0.10.4 fix), plus a persona-corpus fixture resembling a real Hermes default config (agent.personalities with kaomoji/emote strings).

Triage timeline (in case it is useful)

Happy to test a candidate build against the full corpus (the original 4.8 KB config + the A/B battery above). Config fragments above are minimized; identifying details redacted (username/hostname → <user>/<hostname>).

Metadata

Metadata

Assignees

No one assigned

    Labels

    editor/integrationEditor compatibility and CLI integrationwindowsWindows-specific issues

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions