fix: correct server-memory pin to an existing version - #134
Conversation
@modelcontextprotocol/server-memory@0.6.3 does not exist on npm (the version line jumps 0.6.2 -> 2025.4.25), so the memory MCP server has never been able to start from this config on any machine. Pin the nearest existing version.
🔎 Lint report:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 657d8144c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "type": "stdio", | ||
| "command": "npx", | ||
| "args": ["-y", "@modelcontextprotocol/server-memory@0.6.3"] | ||
| "args": ["-y", "@modelcontextprotocol/server-memory@0.6.2"] |
There was a problem hiding this comment.
Move the memory server pin into the MCP template
On a fresh checkout, changing this pin still does not make the memory server available because project MCP definitions belong in .mcp.json, while this repository keeps .claude/settings.json for hooks only; .mcp.json.example still defines only codebase-memory. Move this server definition, including the corrected pin, into .mcp.json.example so copying the template actually registers it.
AGENTS.md reference: AGENTS.md:L26-L30
Useful? React with 👍 / 👎.
…#137) (#138) * feat(deploy): persistent gateway hosting + env-passthrough log hardening Re-lands the reviewed-good content of #137 on a branch cut from current main. #137 could not merge: it was mergeable_state dirty, and it sat on claude/slack-session-94aae0 — the branch of closed #106, which #130 said should not be continued. Two things ship here. tools/env_passthrough.py — CodeQL clear-text-logging fixes. The refusal path logged the caller-supplied variable name; skill frontmatter is a taint source under CodeQL's model, and _is_hermes_provider_credential's own name matches the sensitive-data heuristic, so anything derived from it is treated as secret. Replaced with counts and static strings. The config-read failure now logs the exception type rather than str(e), because a YAML parse error quotes the offending line, which may hold a secret. deploy/, docs/DEPLOYMENT.md, website/docs/guides/persistent-hosting.md, Dockerfile, docker-compose.yml, README.md — running the gateway 24/7 with no long-lived credentials on the host. Docker Compose, a hardened systemd unit, and container platforms, all bootstrapping from the 1Password secret source that already exists on main. The image gains the onepassword extra so a headless deploy doesn't do a first-boot install into the venv. Only placeholder tokens (ops_...your-token...) appear anywhere. Dropped from #137: .claude/settings.json, which reverted @modelcontextprotocol/server-memory from 0.6.2 to 0.6.3. That version does not exist on npm — the published line jumps 0.6.2 to 2025.4.25 — so the revert re-breaks the memory MCP server and undoes #134. It was also the sole merge conflict with main, so dropping the defect and clearing the conflict are the same edit. hermes_cli/config.py is not touched, so the GHSA-mv8x-fg99-32mf _sanitize_env_lines regression #130 warned about is not in play. Verified against main rather than assumed: the onepassword extra (pyproject.toml), every secrets.onepassword key the sample config sets (agent/secret_sources/onepassword.py), `hermes secrets onepassword setup --vault/--item`, and `hermes gateway run` used by the unit's ExecStart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT * docs(website): register the persistent-hosting guide in the sidebar website/sidebars.ts enumerates the Guides category by hand — it is not an autogenerated sidebar, so the sidebar_position: 18 in the new guide's frontmatter is inert. Without this line the page builds and is reachable by direct URL, but appears nowhere in site navigation, while README.md and docs/DEPLOYMENT.md both link its published URL. docusaurus.config.ts sets onBrokenLinks: 'warn', so nothing fails — it just quietly isn't there. Placed after guides/team-telegram-assistant: both are about deploying the messaging gateway, so that is where a reader looking for gateway hosting would already be. Not in #137; found while reviewing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT * test(env-passthrough): pin that registration never logs a variable name The clear-text-logging fix in the previous commit had nothing guarding it. Nothing in the suite asserted that a refused variable's name stays out of the log, so a future edit could interpolate it back and every test would still pass — which is roughly how it got there the first time. Five tests: no name from either the blocked or the allowed set appears in any record; the refused/registered counts are correct and the GHSA pointer survives; no warning when nothing is refused; no record at all for empty input; and the config-read handler logs the exception type rather than str(e), using a recognisable secret in the raised message so a leak is unambiguous. Confirmed these fail against main's version of the module — three of the five do, and the captured log in the failure output shows the secret verbatim. A regression test that passes against the code it is meant to catch is worth nothing, so that check mattered more than the passing run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT * docs(system-log): record the #137 salvage Per docs/system-log/README.md. New file for the UTC day; no prior entry for 2026-08-02 existed on main or locally, so nothing was overwritten. Records what was carried, what was dropped and why, what was added beyond #137, what was verified, and — separately — what could not be verified in a container with no Docker daemon and no website node_modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT --------- Co-authored-by: Claude <noreply@anthropic.com>
…ngs.json
The block was removed deliberately on 2026-07-28 (see docs/system-log) in
favour of .mcp.json.example: Claude Code documents ${VAR} expansion for
.mcp.json and ~/.claude.json but not for settings.json, so a server
declared there needs a literal absolute path and starts on exactly one
machine, failing silently everywhere else. An unrelated dependabot npm
bump (7e38fa5) re-added a copy carrying a hardcoded /Users/<name>/ path.
The `memory` server moves into .mcp.json.example so #134's
@modelcontextprotocol/server-memory@0.6.2 pin survives the removal (0.6.3
does not exist on npm). The `hooks` block is untouched — AGENTS.md is
explicit that settings.json keeps hooks only, and $CLAUDE_PROJECT_DIR
*does* expand there, which is how the tracked
.claude/hooks/session-start.sh is reached.
test_no_repository_local_claude_permissions_file asserted the file must
not exist at all, which would orphan that hook. It now asserts what
AGENTS.md actually requires: no `permissions` and no `mcpServers` key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
* fix: repair two NameError crashes in the Python suite Both are environment-independent source bugs — code paths that could never have run — surfaced while investigating why main's Python suite is red (six of eight CI slices), which blocks the aggregate gate in #186. plugins/memory/mem0: _start_prefetch(self, query) computed `effective_user_id = user_id or self._user_id`, but user_id is not one of its parameters, so any call raised NameError. It is a copy of the identical line in sync_turn fifty lines below, where user_id genuinely is a parameter. The value was never read — the closure filters via self._read_filters() — so the line is deleted rather than the parameter added. hermes_cli/gateway: the launchd plist builder assembled `core_args`, computed a `use_wrapper` flag, then called `prog_args.append(...)` — a name never assigned. All three were mutually inconsistent, so the function could not run to completion. The surrounding comment is unambiguous about intent (the wrapper execs the python + module args passed after it; fall back to direct-python when it is missing) and the three failing tests assert nothing about wrapper shape, so the comment is the spec: the wrapper path goes in front of core_args when usable, core_args alone otherwise. Deliberately not included: the tests importing _expand_value_from_environ and _redact_command_for_display, neither of which exists. Implementing them means inventing semantics in a security-adjacent config loader, and whether they were removed or never written is not answerable from a shallow clone. Also records a correction in the system log: my earlier "69 failing tests" figure was this sandbox, not the repo. Most of it is missing optional extras and a different aiohttp than uv.lock, and some of those failures would be wrong to "fix" — the lazy-deps pin test is correctly detecting local drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix: unshadow the gateway command-line matcher, repair two test harnesses Second instalment of the Python-suite repair. CI confirmed the first: slice 6 went 5 files/20 tests -> 4 files/17 tests with test_mem0_v3.py gone. This clears the remaining four files in that slice. gateway/status.py had TWO definitions of looks_like_gateway_command_line — the canonical one at line 444 delegating to _gateway_command_subcommand (shlex tokenization, profile-selector stripping, case-folding), and a hand-rolled substring scan at line 569. Python keeps the last, so every caller silently got the naive one. looks_like_gateway_runtime_command_line was duplicated the same way. The substring version fails precisely the cases the canonical one exists for: `--profile work gateway run` (the flag splits the substring), `--profile gateway gateway run` (the profile value shadows the subcommand), bare `gateway`, and `GATEWAY RUN`. _scan_venv_blockers' docstring warns about this in as many words, so the regression it describes had reappeared one module over. Deleting the duplicates restores the canonical behaviour unchanged — 14 tests across two files. test_kanban_db referenced bare `hermes_state` at line 907 while line 922 does a plain `import hermes_state`, making the name function-local for the whole body, so the earlier reference could never resolve. It was redundant anyway: line 903 already clears that same set via the _hermes_state alias. test_modal_snapshot_isolation died before reaching its subject: modal.py calls lazy_deps.ensure("terminal.modal") first, and that asks whether the distribution is installed, which a sys.modules stub cannot satisfy. The test already stubs hermes_cli, tools, tools.environments and modal, so lazy_deps is one more stub, not the thing under test. The production check is deliberately untouched — it is security-adjacent, and loosening it to accept pre-imported modules is not a change to make for a test's convenience. Also corrects a claim in the log: I said test_gateway_command_line_matcher fails in CI but passes locally, and used it to argue the environments disagree. It fails locally too — verified by stashing the fix. It was missing from my inventory because the background capture kept only the tail of the full-suite output, so the file list was truncated. No regressions: tests/gateway/ + tests/hermes_cli/ run 9,102 passing / 24 failing across 12 files, none of them files touched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix: define the cron staleness check, stop provider labels collapsing to ids cron/scheduler: gateway/run.py's cron-ticker supervisor imports get_ticker_heartbeat_age and ticker_heartbeat_is_stale from cron.scheduler. Neither existed there — the age reader lives in cron/jobs.py and the staleness predicate was defined nowhere in the repo. The import sits inside the supervisor loop, so the watchdog meant to restart a wedged ticker would itself have died on ImportError the first time it checked. Added the predicate and re-exported the age reader beside it, since the supervisor and its tests both treat cron.scheduler as the ticker's public surface. The rule is taken from the existing docstrings rather than invented: age > interval * stale_multiplier, and an unknown age is NOT stale — get_ticker_heartbeat_age returns None on a missing or torn read and documents that callers treat that as "cannot determine", not "dead". Restarting on a torn read would turn a filesystem hiccup into a restart loop. hermes_cli/providers: get_label("xai") returned "xai", which collapsed the xAI API-key entry onto its own id and made it indistinguishable from the OAuth sibling in the model picker. My first fix was wrong and the test caught it — I added a fallback at the end of get_label, but that line is unreachable for this case: get_provider returns a synthesized def for Hermes-only providers, with name=_LABEL_OVERRIDES.get(canonical, canonical), so the id was already baked in. Fixed at that source via one _display_name() helper shared by both call sites: overrides, then the registry in hermes_cli.models (which already carries ProviderEntry("xai", "xAI", ...)), then the id. Not fixed by adding an _LABEL_OVERRIDES entry: that duplicates a name the registry owns, which is exactly the duplication behind the gateway/status.py bug in the previous commit. models.dev is a remote catalog, so this path is also what every provider hits whenever it is unreachable. No regressions: 774 tests across 90 provider/model files pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * docs(system-log): record the missing config-integrity cluster Five names are imported from hermes_cli.config and none exist; two of them by production code in hermes_cli/backup.py. The restore path swallows the ImportError, so a config restore silently never reseals the integrity baseline and the watchdog reads an authorized restore as tampering. The machinery exists unwired in skills/devops/config-integrity-watchdog/, whose seal(quiet=True) parameter is documented for exactly this caller. Reconstructing it is four functions of security-adjacent behaviour, so it is reported for a decision rather than folded into a make-CI-green pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(slack): assign as_yaml, add the channels:join bot scope hermes_cli/slack_cli.py used `as_yaml` in two places — the render call and the file extension — without ever assigning it, so `hermes slack manifest` raised NameError on every invocation. Every other option in that function is read via getattr(args, ..., default); as_yaml now is too, defaulting to False (JSON), which is what the default write target (slack-manifest.json) and the test's json.loads(stdout) both assume. The generated app manifest also omitted the channels:join bot scope, so `hermes slack invite --all` would fail per-channel with missing_scope on an otherwise valid install — conversations.join requires it. Not fixed here: test_gateway_platform_gating asserts _builtin_setup_fn returns bespoke setup functions for telegram/slack/matrix. Those return None by design — all three moved into plugins/platforms/*/adapter.py, and I verified each registers setup_fn=interactive_setup, so the invariant the test documents (they must not fall through to the generic wizard, which would break Matrix's empty-token password login) still holds. The test asserts the removed route rather than the invariant; replacing it properly needs the plugin registry's lookup API, so it is documented rather than deleted to make CI green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(config): restore the missing config-integrity cluster Eight functions were referenced by tests and production but did not exist in hermes_cli/config.py: get_config_lock_path, get_config_seal_path, config_write_lock, config_read_lock, seal_config, verify_config_integrity, _reseal_git_backed_integrity_baseline and restore_config. Three test files failed at collection because of it, and hermes_cli/backup.py imported two of them behind `except ImportError: pass` — so a config restore silently never resealed and the watchdog read it as tampering. They were deleted, not never written: config.py still imports `fcntl as _fcntl` and `msvcrt as _msvcrt` at module scope and uses neither, which is exactly what the lock helpers needed, and hermes_cli/config_integrity_cli.py still provides the _find_core_module bridge the tests import. save_config() now seals both baselines after a successful write — the local config.yaml.sha256 sidecar and the git-backed JSONL log in the operator's dotfiles repo. A write through save_config is authorized by definition, so leaving those stale is what made the watchdog flag the model scanner. Decisions worth calling out. A missing config and a missing seal both verify OK: absence of a baseline is not a mismatch, and failing there would flag every pre-existing install as compromised. Sealing never fails a write — the git-backed half shells into a repo outside Hermes' control that can be absent, non-git, mid-rebase or read-only. That reseal is a no-op unless the dotfiles directory already exists, since the skill's seal() would otherwise mkdir a baseline on machines that never opted in. Locks degrade to no-lock rather than raising, and Windows readers go unlocked because msvcrt has no shared mode. restore_config always writes a pre-restore backup, including when the live file is unreadable — a corrupt config is the usual reason a restore is happening. _expand_value_from_environ leaves an unset reference verbatim rather than blanking it, so a typo'd variable stays legible instead of becoming an empty API key. That one rule also resolves the apparent contradiction in its tests: $TEST_VAR expands and literal-$var does not, purely because one name is exported and the other is not. It is not wired into config loading — _expand_env_vars walks whole documents and resolves ${env:NAME} SecretRefs with different warning behaviour, so delegating would change semantics rather than restore a helper. Verified against the same baseline command as before: tests/gateway/ + tests/hermes_cli/ go from 9,102 passing / 24 failing across 12 files to 9,139 passing / 11 failing across 8, with collection errors down from 3 to 1. Strictly better on every axis, so the new sealing side effect regressed nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(config): open the lock file in binary mode Ruff (PLW1514) and the Windows-footgun checker both flagged the new `open(lock_path, "a+")`: text mode without an explicit encoding, which on Windows also brings newline translation. The file is only ever a lock handle — never read or written as text — so binary sidesteps both concerns rather than papering over them with an encoding argument. Both checks pass locally: `ruff check hermes_cli/ cron/ gateway/` is clean and `scripts/check-windows-footguns.py --all` reports no footguns across 927 files. Unrelated, for the record: the `gitleaks (diff)` failure on 25b74f9 is not a secret. Its install step died on `curl: (22) ... error: 503` fetching the release tarball, so the scanner never ran — but the job's failure-notice step prints "gitleaks flagged a secret in this diff" unconditionally, making a transient CDN failure look identical to a real detection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(model-metadata): define the Ollama /api/show probe gate Ninth instance of the same defect class: _should_probe_ollama_api_show was referenced by tests but defined nowhere, so the file raised NameError. Regression guard for NousResearch#31555 — /api/show is an Ollama-server endpoint, so firing it at a hosted provider costs a doomed HTTP roundtrip (up to the 3s probe timeout) on a path that runs during agent startup. The rule comes from the three tests: skip for a known hosted provider, infer the provider from the base URL when none is named (which is how the OpenRouter case reached the probe at all), otherwise allow. The first attempt gated on _PROVIDER_PREFIXES and was wrong — that set contains plain `ollama`, because it answers "can this appear as a model prefix?", not "is this hosted?". test_allows_ollama_provider caught it. Local providers are now checked first via an explicit _LOCAL_OLLAMA_PROVIDERS; ollama-cloud stays out of it, being the hosted service that serves no /api/show. Found by reading CI's slice list rather than my sandbox: tests/agent/ — 351 files — had never been in any of my local sweeps, so this bug was absent from every inventory I had built. That also corrects my earlier claim that the remaining failures were all environmental: the environmental ones are real (root defeats chmod-based permission tests, no IPv6 for bind assertions), but that was a statement about a set I had not measured. tests/agent/: 3,926 passing / 12 failing across 7 files, none of them this one. ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * docs(system-log): complete local inventory and two corrections First full-suite run with a repaired venv — the first complete enumeration this session has had. 62 failures across 28 files, roughly half of them this sandbox's missing optional extras rather than repo bugs. Two of the real ones are features that do not exist rather than bugs: _redact_command_for_display (tools/approval) and per-model credential lockout (CredentialPool.select/_mark_exhausted take no model kwarg and the module has zero lockout machinery, against a 185-line spec). Both left for a decision. The contrast with the config-integrity cluster is the reason: that one had orphaned fcntl/msvcrt imports, a surviving bridge module and a skill implementing the machinery, so restoring it was recovery. There is no such trace here, and inventing cooldown semantics beside a DEAD/EXHAUSTED state machine that governs billing-bearing credentials is a feature decision. Also records two corrections. My first regression check was vacuous — git stash takes only uncommitted work, so with everything committed I compared the branch against itself and read the identical numbers as evidence. A worktree at origin/main gives the real answer: the tests/agent/ failures are pre-existing. And 'same compromised run' was wrong; runs are mixed, and log length (~180 vs ~1000+ lines) discriminates a setup death from a real test run, not the run id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(deps): stop lazy installs downgrading aiohttp below its CVE patch pyproject.toml and uv.lock both pin aiohttp==3.14.3; tools/lazy_deps.py still pinned 3.14.1 in four places, and cbor2==6.1.2 against a uv.lock of 6.1.3. The consequence is in the failing test's own message: pyproject extras pins must match LAZY_DEPS pins for every shared package, otherwise `hermes update` downgrades the package. Every one of those lines carries a CVE list including an RCE (CVE-2026-34993), so installing any lazy feature — a Discord, Teams or Matrix platform, or the Modal terminal — would have pulled aiohttp back below the patched floor on a machine that already had the fix. Bumped the four aiohttp pins to 3.14.3 and cbor2 to 6.1.3, matching uv.lock exactly. Also corrected the comments that named the old version beside the new pin (`# aiohttp 3.14.1: CVE-…` next to `aiohttp==3.14.3`, once here and three times in pyproject.toml) — a version string that disagrees with the pin it annotates is the drift that produced this. Correction to my own triage: I had filed these two tests under "sandbox drift" because the message mentions an aiohttp version, assuming it reflected my stale venv. It does not — they compare declarations across pyproject/lazy_deps/uv.lock and never inspect what is installed, and CI fails them identically. test_project_metadata + test_packaging_metadata: 13 tests, 0 failures. pyproject.toml parses; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * feat(credential-pool): restore per-model credential lockout A 429 for one model says nothing about the others, but the pool could only bench a credential globally — so one rate-limited model removed the key from rotation everywhere. Correction to my own earlier triage: I reported that credential_pool.py had "zero model-lockout machinery" and deferred this as inventing a feature. That was wrong — I grepped for model_lockout/locked_models/model_locks; the feature is called exhausted_models, and two pieces were already shipping. _model_exhausted_until() lives in credential_pool.py and reads exactly the record shape the tests specify, and hermes_cli/auth_commands.py renders entry.extra["exhausted_models"] in auth status output. The display side has been live against a field nothing ever wrote — the same partial-deletion signature as the config-integrity cluster, consumers and helpers surviving without the producer. _mark_exhausted(model=...) now records the bench in extra["exhausted_models"] and leaves the credential-wide status alone. Terminal auth failures are deliberately excluded from that path: a revoked or invalid token is dead for every model, and recording it per-model would keep the credential in rotation for the rest, failing instantly on each. The existing _is_terminal_auth_failure classifier makes that call, so the DEAD/EXHAUSTED distinction is unchanged. select(model=...) filters entries benched for that model, keyword-only so existing no-argument callers are untouched. _prune_expired_model_lockouts() drops lapsed records, mirroring what _available_entries(clear_expired=True) does for the credential-wide status, and runs before filtering so a just-expired lockout frees its credential in the same call. reset_statuses() now counts a per-model lockout as status — a credential can be benched for a model with every credential-wide field clear, and reset must free that too rather than reporting "0 reset". tests/agent/test_model_lockout.py 4/4. Credential, pool and auth suites: 477 tests across 67 files, 0 failures. tests/agent/ overall 3,930 passing / 8 failing, up from 3,926 / 12 — no regressions in a module governing billing-bearing credential rotation. ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(approval): redact commands shown for approval; fold single-component homes Two fixes in tools/approval.py, five tests. _redact_command_for_display was missing. Searching for consumers first — the lesson from the credential-pool commit — showed agent/redact.py already carries the Slack xapp-/xox[baprs]- patterns, URL-userinfo handling and redact_sensitive_text(), and that it satisfies every assertion in the spec unchanged. So the missing piece was a thin wrapper, not a redaction engine. It earns its own name from the contract in the test docstring: the original command still runs, this is display-only. Motivated by the 2026-07-13 incident — a heredoc appending a Slack app-level token to ~/.hermes/.env, where the approval prompt reproduced the command in full, so asking permission published the credential more widely than running it would have. The variable name survives so the reviewer can see which secret is being written; only the value is masked. force=True and redact_url_credentials=True come from redact_sensitive_text's own docstring — force is documented for "safety boundaries that must never return raw secrets regardless of the user's global logging redaction preference", and an approval echo into Slack is an egress boundary rather than the tool flow the URL default protects. The fifth failure was TestSensitiveRedirectPattern, and it corrects me twice. I first checked /home/alice/.ssh/authorized_keys, saw False, and called it a general security hole — wrong, that is not the current user's home and the fold is deliberately scoped to the running user's. The real issue is narrower: _home_prefix_fold_regex required two components below the root, which correctly rejects / and /home (folding /home would rewrite every user's path to ~), but /root is a legitimate home with one component. So on any root-run deployment — the Docker image, CI containers, sudo — ~/.ssh/authorized_keys was guarded while the identical /root/.ssh/authorized_keys was not. Replaced the count with the property the guard wants: reject a bare root, and reject a single component only when it contains other homes (home, users, var, mnt, …). Verified both directions: /root/.ssh/authorized_keys now flags, ~ still flags, /home/other/.ssh/authorized_keys is still left unfolded. test_approval.py 101 passing / 0 failing (was 96/5). tests/tools/ 5,375 passing / 18 failing across 3 files, all pre-existing: daytona (optional extra absent), execution-flag detection, and the chmod-under-root artifact. ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(cron,state): re-enable the privileged-model gate and the PASSIVE WAL checkpoint Three defects of the same shape: the mechanism survived, the thing that uses it did not. cron/model_policy.py is a complete, documented guardrail — it refuses to route a legal/finance cron job to a denylisted or non-allowlisted provider/model. It had zero callers. `enforce_privileged_model_route()` now runs in create_job() on the assembled record and in update_job() on the *merged* record (the privileged tags come from the stored prompt/skills while the offending route arrives in the update, so neither half is conclusive alone). Both raise before save_jobs(), so a rejected job never reaches disk. hermes_state._try_wal_checkpoint() executed `PRAGMA wal_checkpoint(TRUNCATE)` under a 16-line docstring explaining that TRUNCATE is what caused B-tree corruption on large databases (NousResearch#45383) and that the periodic path must be PASSIVE. The warning string and the tests still said PASSIVE; only the pragma had reverted. Now PASSIVE again; close() and the pre-VACUUM path keep TRUNCATE, as documented. test_gateway_platform_gating asserted the retired lookup: telegram/slack/ matrix moved to plugin-registered `setup_fn=interactive_setup` in NousResearch#41112, and _configure_platform() tries the registry entry first. The invariant — these three never fall through to _setup_standard_platform(), whose mandatory-first-var behaviour would break Matrix's password-login path — is now asserted against the mechanism actually in use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(config): drop the resurrected mcpServers block from .claude/settings.json The block was removed deliberately on 2026-07-28 (see docs/system-log) in favour of .mcp.json.example: Claude Code documents ${VAR} expansion for .mcp.json and ~/.claude.json but not for settings.json, so a server declared there needs a literal absolute path and starts on exactly one machine, failing silently everywhere else. An unrelated dependabot npm bump (7e38fa5) re-added a copy carrying a hardcoded /Users/<name>/ path. The `memory` server moves into .mcp.json.example so #134's @modelcontextprotocol/server-memory@0.6.2 pin survives the removal (0.6.3 does not exist on npm). The `hooks` block is untouched — AGENTS.md is explicit that settings.json keeps hooks only, and $CLAUDE_PROJECT_DIR *does* expand there, which is how the tracked .claude/hooks/session-start.sh is reached. test_no_repository_local_claude_permissions_file asserted the file must not exist at all, which would orphan that hook. It now asserts what AGENTS.md actually requires: no `permissions` and no `mcpServers` key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * docs(system-log): ninth instalment — CI's failure list and the last five Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix(cron): anchor privileged-workload keywords so ordinary jobs aren't refused Found by the E2E validation AGENTS.md requires before wiring a dead module into a live path — and it would have shipped a regression. detect_privileged_workload_tags() scanned for its keywords as plain substrings. Several of them are acronyms: "je" (journal entry) matches "project", "subject", "rejected" and "Jenkins", and "books" matches "notebooks". Harmless while the detector had no callers. Now that the route gate runs on create_job()/update_job(), an over-broad match doesn't just mislabel a job — a privileged tag plus any pinned provider/model outside the six-entry allowlist refuses the job outright. "Summarize the project changelog" with a model override would have been rejected. Keywords now match at a token start, so inflections still tag ("reconciliation" -> "reconciliations", "attorney" -> "attorneys") while a keyword buried inside an unrelated word does not. Acronyms are anchored at both ends. The over-tagging the module documents as deliberate is intact: every keyword that matched a real word before still matches. E2E against a temp HERMES_HOME through the real create_job/update_job chain: an ordinary pinned job saying "project" lands, a denied route is refused without persisting, an allowlisted privileged route lands, a bad update leaves the stored job untouched, and ordinary updates still work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * docs(system-log): tenth instalment — E2E validation caught a regression Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * test(moa): remove the interrupt-ordering race in the reference fan-out test test_references_parallel_interrupt_aborts_wait raised the interrupt flag from inside the "fast" stub, before that stub's response reached its future. A poll landing in the gap saw an interrupt with the reference still in flight, so the slot was marked skipped and the assertion failed — rare locally, hit on a loaded CI runner. The production path is not at fault: _run_references_parallel() records completed futures before it checks the flag, and the interrupted sweep preserves any result that lands in between. The test simply wasn't creating the scenario it described. The interrupt now comes from the progress_callback the fan-out invokes right after recording a completed reference — "the interrupt arrives right after the fast reference finishes", ordered by the production code instead of by thread scheduling. Verified in both directions: forcing the old ordering with a 150ms linger reproduces the exact CI failure string, and the new ordering under the same delay returns the real output. agent/moa_loop.py is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 --------- Co-authored-by: Claude <noreply@anthropic.com>
What does this PR do?
One-line fix:
.claude/settings.jsonpins the projectmemoryMCP server at@modelcontextprotocol/server-memory@0.6.3, a version that does not exist on npm — the published version line jumps0.6.2 → 2025.4.25. Thenpx -ylaunch therefore 404s and the server has never been able to start from this config on any machine (Mac or remote). Repinned to0.6.2, the nearest existing version to the intended pin.Related Issue
Surfaced while diagnosing why the repo's configured MCP servers don't start in Claude Code remote sessions.
Type of Change
Changes Made
.claude/settings.json—@modelcontextprotocol/server-memory@0.6.3→@0.6.2How to Test
Generated by Claude Code