Package-facing patch bump so 13.24.20 ships the commits now on main after v13.24.19. Rebuilds plugin bundles so the injected version matches.
Does not npm publish.
Package-facing patch bump so 13.24.19 ships Batch II leftovers now on main after v13.24.18. Rebuilds plugin bundles so the injected version matches.
Does not npm publish.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Package-facing patch bump so 13.24.18 ships the commits now on main after v13.24.17. Rebuilds plugin bundles so the injected version matches.
Does not npm publish — Prioritizer publishes from tag.
Package-facing patch bump so 13.24.17 ships #4026/#3575 (health probe deadline cap), #3445 (desktop-bundled Codex CLI on macOS), and #4027 (register memory_session_id, do not re-register). Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Package-facing patch bump so 13.24.15 ships #3727 / #3706 (daemon no longer inherits user project cwd). Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Package-facing patch bump so 13.24.14 ships the commits now on main after v13.24.13. Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Pin shell-quote to 1.9.0 in the plugin manifest, lockfile, root
devDependency, and build-hooks generator so a normal build cannot
regenerate the CVE-affected ^1.8.3 range.
Rebase of #3646.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Package-facing patch bump so 13.24.13 ships the commits now on main after v13.24.12. Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Pass Node/undici { verbose: true } from the existing worker-utils fetch wrappers when CLAUDE_MEM_FETCH_VERBOSE is 1/true/on/yes. On failure, log the serialized error cause chain. Default fetch behavior is unchanged.
Closes#3957
Package-facing security patch for the #3861 / #3985 credential-leak fixes
(unauthenticated /api/settings redaction, MCP workspace containment, host
bind tightening). Publish-ready; do not npm publish from this PR.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Quota cooldown is an intentional pause, not a failure, so observer-health
stayed green and the session-start banner stayed silent while the worker
queued work. Mirror the breaker into observer-health.json and show a
pause notice on the existing health/banner path without changing
queue-accept or drain-on-clear.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Rebase the three leak-path fixes from #3680 onto current main without the
stale plugin-bundle merge or a global requireLocalhost (that would break
Observation TV's remoteReadOnly guard).
- Mask known secrets on unauthenticated GET /api/settings; skip POST writes
that are exactly the stored secret's mask so a viewer save cannot clobber
a key, while still accepting a replacement that happens to start with '*'.
- Confine smart_search / smart_unfold / smart_outline to the workspace,
realpathing both sides so an in-workspace symlink cannot escape.
- Reject arbitrary IPv4 CLAUDE_MEM_WORKER_HOST values. Loopback and the
documented 0.0.0.0 / :: bind-all addresses stay valid for Docker and
Observation TV.
Closes#3861
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* fix(hooks): stop login-shell PATH rebuild on Claude Code hooks
Runtime hooks now share the Setup hook NVM/common-bin PATH prepend instead of spawning $SHELL -lc on every invocation (#3190).
Also drop the stale SessionStart trailing JSON echo; start already prints its own status payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(hooks): add NVM PATH prelude guards for #3190 bot edge case
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(hooks): assert NVM PATH prefix instead of command -v node
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hooks): correct NVM ls quoting in PATH prelude (#3190)
Backslash-escaped inner quotes broke ls under bash -c, so the NVM bin directory was never prepended. Regenerate hooks.json and harden shell-eval tests for Git bash /tmp path mapping on Windows.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
A write to the spawned bun child's stdin can race the child exiting,
producing an EPIPE. With no 'error' listener on the Socket, Node
re-throws it as an unhandled exception and crashes the hook wrapper.
Add a no-op error listener before the write block so the EPIPE is
silently absorbed — consistent with the script's existing exit-0-on-error
contract for hook failures.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Claude Code fires SessionStart with source "resume" on session resume
(claude --resume / --continue). The hooks.json matcher "startup|clear|compact"
did not include "resume", so resumed sessions skipped the worker-service
start and context injection hooks entirely — no prior memory was injected.
The codex-hooks.json already includes "resume" in its matcher. This brings
hooks.json into parity.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* fix(setup): guard plugin deps on completeness, not node_modules existence
`ensurePluginDependencies()` decided whether to run `bun install` by asking
whether `node_modules/` existed. A tree that is merely present — but short
of the declared closure — satisfied that check and permanently skipped
repair on every subsequent Setup run.
Two trigger paths, and the second needs no corruption at all:
1. An install interrupted mid-fetch (network timeout, OOM, registry 5xx)
leaves `node_modules/` behind incomplete.
2. A tree that was complete *for the version that created it*. `zod` was
added to plugin deps after some users had already installed; their
node_modules has been incomplete ever since, and no upgrade heals it
because the stale tree is gitignored and gets re-seeded into each new
cache version.
The worker then dies at boot on `Cannot find module 'zod/v3'` while memory
search keeps working — `mcp-server.cjs` bundles zod (build-hooks.js:519
hard-fails if it ever externalizes it) while `worker-service.cjs` has 19
external zod requires. So the plugin looks alive while capture is dead.
One reporter lost ~4 months of capture with no visible symptom.
Guard on completeness instead: every key of `package.json` `dependencies`
must resolve, with a `<dep>/package.json` fallback for bin-only packages
like tree-sitter-cli (gh #2730), plus the zod subpaths the worker requires.
This mirrors `verifyCriticalModules` (src/npx-cli/install/setup-runtime.ts:245),
which already applies exactly this contract on the npx install path but was
never reachable from the Setup hook. It cannot be imported here — this
script is standalone and dependency-free — so the probe is inlined and the
two are cross-referenced.
Two consequences fall out:
- The post-failure `rmSync` of node_modules is removed. It existed only
because the existence guard would otherwise block retry forever; the
completeness guard re-detects the gap on the next run, so deleting bought
nothing while actively destroying a partial tree that still powers search.
- A zero exit from `bun install` is no longer trusted. It can exit 0 with a
failed integrity check, so the closure is re-probed afterwards and the
diagnostic reports what actually resolves.
The install diagnostic now names the unresolvable modules, which is what
stops this failure mode from being silent.
Fixes#3755. Refs #3604 (plan-16), #2730.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NT5K64VU4a7Kbc36oTVjyc
* fix(setup): keep the completeness probe inside the plugin's own node_modules
Greptile P1 on #3872, and it was right.
`require.resolve(dep, { paths: [nodeModulesPath] })` reads as tree-scoped
but is not. `paths` only seeds Node's lookup; resolution then walks every
ancestor directory and always consults the global folders
($HOME/.node_modules, $PREFIX/lib/node). Plugin roots live at
~/.claude/plugins/cache/thedotmack/claude-mem/<version>/, so a copy of a
dependency anywhere above them — or installed globally — answered for the
plugin's own.
Verified against the committed probe: a plugin whose node_modules is
completely EMPTY, with a valid zod one directory up, exits 0 with no output
and never runs the install. That is the #3755 bug reintroduced by the fix
for it, and it would have shipped silently.
Presence is now checked by statting `<node_modules>/<dep>/package.json`
directly, which cannot escape the tree. This is also the signal
scripts/check-postinstall-allowlist.js:75-78 already uses, and it handles
scoped names (split into path segments) and bin-only packages like
tree-sitter-cli (package.json present, no entry point — gh #2730) without
the bare-name/fallback dance.
zod's subpaths still need real resolution, since they are `exports` entries
that a present directory does not guarantee. Those are now accepted only
when the resolved file lands inside the plugin's own zod directory. Both
sides are realpath'd before comparison: bun can materialise node_modules
entries as links into a shared store, and Node returns the real path of
what it resolved, so a literal comparison would report a healthy linked
install as missing and loop the install forever. Containment uses
path.relative rather than string prefixing so a sibling like zod-extra is
not mistaken for being inside zod.
Regression test added; it fails against the previous commit's probe.
Note for follow-up: verifyCriticalModules (setup-runtime.ts:245) has the
same ancestor/global escape. It is far less dangerous there — a post-install
assertion that fails loud, not the gate deciding whether repair runs at all
— but worth tightening.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NT5K64VU4a7Kbc36oTVjyc
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
In Codex, memory is never re-injected after a context compaction. The
SessionStart hook that runs `hook codex context` matches only
"startup|resume", so after a manual /compact or an auto-compaction the
hook does not fire and the session continues with an empty context. A
long-running Codex session loses all injected memory at its first
compaction and never gets it back.
Codex emits four SessionStart sources, not two — SessionStartSource in
codex-rs/hooks/src/events/session_start.rs is Startup, Resume, Clear,
Compact. clear and compact both hand the model a fresh context, which is
exactly when the injection has to run. The Claude Code config already
matches "startup|clear|compact" for the same reason.
Add the two missing sources to the matcher. The hook command is
unchanged and does not branch on source, so it injects on compact and
clear exactly as it already does on startup and resume.
Closes#3862
`runBatchQuery()` shells out with `tree-sitter query -p <grammar-dir>`, and
`-p/--grammar-path` is documented as implying `--rebuild`: the CLI recompiled the
grammar from C on every invocation. Measured with tree-sitter-cli 0.26.9 on Linux
x86_64, one `smart_outline` over a 933-line TypeScript file cost 734/711/704 ms
across three runs, nearly all of it the compile. The artifact the CLI wrote to
its own cache was rewritten each time and never read back.
Grammars are now built once into <data-dir>/tree-sitter-libs and queried with
`-l <lib> --lang-name <language>`. The same three runs become 733/16/12 ms and
return the identical 30 symbols; the first call still pays one build.
Because `parseFilesBatch()` issues one CLI call per language group, the old cost
was a fixed ~0.7s per language per tool call regardless of batch size, paid alike
by smart_outline, smart_search and smart_unfold, and repeatedly by anything that
walks a repository.
The artifact lives in the data dir rather than in node_modules: an update
replaces node_modules wholesale, and those directories belong to the installer.
Staleness is an mtime comparison against src/parser.c and the optional scanner —
npm and bun both stamp installed files with the install time, so a plugin update
invalidates the previous artifact on its own. The check is re-run per call rather
than memoized, since four stats cost nothing next to the spawn they guard and a
memo would pin a long-lived MCP server to the grammar that was current at boot.
Every failure path falls back to the previous `-p` invocation: a build that does
not compile, or a grammar whose language function is not named after our language
key. The opt-out is remembered per language, so a mismatch costs one extra spawn
once instead of two per batch forever. All 24 shipped grammars were verified to
bind under their existing GRAMMAR_PACKAGES key, including the irregular ones
(typescript, tsx, kotlin, php, scss, sql, markdown).
Fixes#3926
Co-authored-by: rorar <rorar@users.noreply.github.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Persist unsuccessful version recycles across hooks, retry after the bundle changes, and rebuild shipped workers. Check committed bundle versions before CI rebuilds them.
Refs #3940
* docs(ccs-align): Phase 3 verify/sign-off — mark Phases 0-2 shipped, honest 'what this is not', worker-lag ops MISS
Phase 3 closes the plan loop with no new runtime surface:
- SKILL.md: heading + status now reflect Phases 0-2 shipped (#3934/#3935/#3936)
and Phase 3 = verify/sign-off; add explicit 'What this is NOT' (no compiler,
no brainbeat product, no attention trough, no Focus/mouth, no second LFG
writer, no history rewrite); record running-worker 13.24.1 vs repo version lag
as an ops MISS to roll up (worker restart is a hard forbid for this seat).
- plan: status PLAN ONLY -> PASSED & SHIPPING (Phases 0-2 merged; Phase 3 verify).
Defaults table and history preserved.
- Address the human as Alex.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* chore: bump version to 13.24.5
PATCH bump for CCS Align Phase 3 sign-off (D10 — skill + plan ship, no
product claim). CHANGELOG.md left untouched (generated).
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* feat(ccs-align): Phase 2 rules alignment — house → project → seat conflict walk
Walk house → project → seat layers, detect four conflict classes
(SHADOW_HOUSE, DENY_ALLOW, DRIFT, CLOCK_HEADER), emit an append-only
rules-report.md, and optionally apply SHADOW_HOUSE leaf patches
when CLAUDE_MEM_CCS_ALIGN_PATCH_SHADOWS=true.
- CcsAlignRulesWalker.ts: cascade rules checklist (not a parser),
layer walk with MISS recording, conflict detection, atomic report
append, gated shadow patch with forbidden-target guards
- 26 tests covering all four conflict classes, patch on/off,
standing/always/never safety, MISS on absent paths, append-only
report, edge cases
- SKILL.md updated with Phase 2 docs, conflict table, programmatic
usage, cadence note (every 6th hour), and verification greps
Implements plans/2026-09-09-ccs-align.md §2.1–2.3.
No Focus, no attention trough, no .cas compiler, no history deletes.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* chore: bump version to 13.24.4
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Phase 1 of the CCS Align plan of record (plans/2026-09-09-ccs-align.md §1.1–1.4):
Exclude marks filter the *compiled* middle cache — the diary / SQLite stay
authoritative. A mark records observation ids and tool-use ids; the
grab→append→replace pipeline drops marked records so they never appear in
the compiled middle.jsonl. Unmarking + rebuild restores them from the diary
on the next pull. DELETE /api/observation/:id remains FORBIDDEN.
Implements:
- ExcludeMark type + exclude-marks.json schema (v:1)
- readExcludeMarks / writeExcludeMarks / addExcludeMark / removeExcludeMark
- buildExcludeSet for the atomic pipeline
- appendMiddleCacheRecordsAtomic now filters by exclude set
- landObservationsInMiddleCache loads marks and applies them
- rebuildMiddleCache for unmark+rebuild path
- Skill updated for Phase 1: exclude-marks section, layer-4 get_tool_uses
warning (mark-time only), viewer isolation, unmark+rebuild, purge tools
- 11 new Phase 1 tests: mark drop, diary present, tool ids never in
middle.jsonl, viewer isolation, unmark+rebuild, exclude-marks round-trip,
buildExcludeSet, secure-isolation reason, corrupt marks fail-closed,
marked ids skipped on ingest
Hard forbids verified:
- No DELETE /api/observation (compile-time omit, not tombstone)
- No LFG/Orifice [awareness] writes
- No profile.md touch
- No sixth processAgentResponse consumer
- No CHANGELOG hand-edit
- No 'Az' in user-facing strings
PATCH bump: 13.24.2 → 13.24.3
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* feat(ccs-align): seat-owned middle cache helper + settings (Phase 0)
Copy the #3931 atomic append primitive (appendAwarenessLineAtomic /
awarenessLineBody / formatAwarenessLine) into a seat helper with the three
locked Phase 0 changes: tag [ccs-align], seat-owned path root
~/.claude-mem/ccs-align/<viewerId>/, and a middle.jsonl store. Grab -> append
-> replace is atomic (temp + renameSync), deduped by observation id and by
date-excluded body. Path safety refuses profile.md, agents/**/memory/log, and
any write outside the seat root; the lander never throws into the caller.
Adds CLAUDE_MEM_CCS_ALIGN_{ENABLED,VIEWER_IDS,TRIGGER_TYPES,PATCH_SHADOWS}
defaults (needle types copied from the Grok list per D6).
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* docs(skill): add ccs-align SKILL.md with hourly Worker Watch runbook
Phase 0 breathing slice: resolve worker port (timeline-report snippet),
prefer GET /api/health, pull search -> timeline -> get_observations, land
observations in the seat middle cache, update cursor.json. Documents the
Appendix A hourly cycle, settings, hard forbids, and later-phase stubs.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* test(ccs-align): middle-cache format/needle/append/dedupe/path-safety
Copies the #3931 pusher test patterns: [ccs-align] format + 500-char
truncate, needle match, atomic append, id/body dedupe across days, path
safety (never profile.md / agents/memory/log / outside root), never-throw,
D2 append-only fallback, and cursor.json round-trip.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* chore: bump version to 13.24.2 (PATCH — CCS Align Phase 0 code ships)
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* chore: sync marketplace.json + plugin/package.json to 13.24.2
Version Consistency CI flagged .claude-plugin/marketplace.json still at
13.24.1 after the 13.24.2 bump. sync-plugin-manifests.js does not stamp
marketplace.json or plugin/package.json, so bring both source files in line
with root package.json. The worker-service.cjs stamp is regenerated by
'npm run build' in CI before the test runs.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Raw tool I/O had no durable home. `pending_messages` is the generation
queue -- rows are claimed, summarized, and deleted -- so once an
observation existed the original tool_input/tool_response were gone.
Adds `tool_uses` (schema v51) as a by-reference side index for those
bodies, written from the one ingest choke point both the PostToolUse hook
route and the transcript-watch processor already share. The JSONL
transcripts and `src/services/transcripts/*` remain the spine and are
untouched.
Schema (Receipt freeze 2026-09-06): UNIQUE(content_session_id,
tool_use_id), nullable `or_generation_id` / `or_session_id` as join keys
back to an OpenRouter spend line, and deliberately no cost_usd/micros --
this table carries tool identity, dollars stay on the OR stamp. No FK on
session_db_id/observation_id: a FK there can abort the constructor
migration chain (#3378), and observation_id is linked late by design.
Write path dual-writes alongside -- never instead of -- the
pending_messages enqueue, and swallows its own failures so an observation
is never lost to a backup-index error. `toolUseId` now actually reaches
the worker from hooks: it was missing from NormalizedHookInput and every
hook adapter, so only the transcript path supplied it. ResponseProcessor
links the batch's claimed ids to the first stored observation.
Read path is progressive-disclosure layer 4: POST /api/tool-uses/batch
requires explicit ids (never a full-table scan), GET /api/tool-uses
returns a cheap index shape with size hints and never the payloads, and
the `get_tool_uses` MCP tool is described as a last resort. claude-mem's
own read tools are skipped by the writer -- without that, every call to
get_tool_uses would store the bodies it just returned.
Payloads are stored in-row with a 64 KB soft cap and a UTF-8-safe
truncation marker; content_hash is computed over the original.
Join contract for Receipt: RECEIPT-JOIN.md
Claude-Session: https://claude.ai/code/session_01QgdJ6gExgBirDoAxknt94m
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(ui): observation TV — fullscreen fading titles off the existing SSE stream
Adds a standalone, dependency-free page that consumes the same /stream the
React viewer does and plays each observation's title as a fullscreen fading
card. Live arrivals play first; a seeded backlog from /api/observations cycles
while the worker is idle, so the screen is never blank.
Picture-in-picture without a broadcast library: Document PiP (Chromium) moves
the real DOM into the floating window so the CSS fades keep running, and
everywhere else — including iOS Safari, the phone case — the card is painted
to a canvas whose captureStream() feeds a muted video into native PiP.
Served two ways: express.static already exposes plugin/ui, so /tv.html works
with no route change, and a /tv alias is cached at boot the same way
viewer.html is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6QPdnPducVehMwCM2HYNC
* docs(plans): observation TV read-only broadcast + shared-secret token
Phased plan for the locked 2026-09-05 decision: expose Observation TV to a
second device on the LAN without exposing the rest of the worker.
The worker has no request authentication anywhere; its only defence is the
loopback bind, and the codebase says so out loud (ServerService.ts:129-131).
So CLAUDE_MEM_WORKER_HOST=0.0.0.0 today does not put the TV on the LAN, it
puts GET /api/settings — which returns the user's Gemini and OpenRouter API
keys in plaintext — on the LAN, alongside the settings writer, the row
deletes, bulk import, and better-auth's key issuance.
The design is one guard middleware mounted at position zero in the Server
constructor, the only spot that covers /api/auth/*, /api/admin/*, the static
mount, and every route registered later. It is a no-op for loopback and, for
non-loopback requests, default-deny with a four-path exact-match allowlist
behind a new CLAUDE_MEM_TV_TOKEN. An empty token means the guard is never
mounted, so every existing install — including the documented Docker 0.0.0.0
setup — is byte-identical to today.
Phase 0 is written out rather than delegated: ~45 routes inventoried with
file:line, the copy-ready patterns named (requireLocalhost, parseBearerToken,
safeEqualHex, the securityHeaders opt-in precedent), and five traps recorded,
including that SettingsDefaultsManager.get() cannot see settings.json and that
the worker never calls finalizeRoutes() so the guard must write its own
responses. Appendix B lists every rejected option with its reason —
cloudflared first among them.
Plan only. Nothing implemented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMh2GZST1UgKDSML17qCmh
* feat(worker): read-only Observation TV broadcast behind CLAUDE_MEM_TV_TOKEN
The worker's HTTP surface (45+ routes) has no request authentication; the
loopback bind is its only defence. So setting CLAUDE_MEM_WORKER_HOST=0.0.0.0 —
which the Docker docs tell people to do — puts GET /api/settings (provider API
keys in plaintext), POST /api/admin/restart, DELETE /api/observation/:id,
POST /api/import and better-auth on the LAN.
Add one guard middleware, mounted at position zero in the Server constructor —
the only spot that covers /api/auth/*, /api/admin/*, the static mount and every
route registered later, including routes that do not exist yet. It is a no-op
for loopback and, for non-loopback requests, default-deny with an exact-match
four-path allowlist behind a shared secret:
/tv, /tv.html, /stream, GET /api/observations
A GET/HEAD method gate kills every mutation; non-allowlisted paths get 404 so a
scanner is not told which routes exist; the token is compared constant-time and
accepted as Authorization: Bearer, X-Api-Key, or ?token= (the query form exists
only because EventSource cannot set headers). The token is never logged.
Empty token means the guard is never mounted, so every existing install behaves
exactly as before and CLAUDE_MEM_WORKER_HOST keeps its 127.0.0.1 default. A
boot-time SECURITY warning fires when the host is non-loopback with no token —
warn, not refuse, so the documented Docker deployment keeps working.
Also fixes createCorsMiddleware forwarding next(new Error('CORS not allowed')):
the worker never calls finalizeRoutes(), so that reached Express's default
handler and returned a 500 HTML stack trace with absolute filesystem paths —
newly reachable from the LAN. It now writes its own 403 JSON.
tv.html carries the token through to both of its calls, and cards now show
platform_source with a per-source accent colour in both the DOM and canvas
render paths.
No new dependencies. 38 tests in tests/server/tv-remote-guard.test.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xcn8Gf6ACkfDqLYaULAj2k
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patch release so marketplace and npm installs actually pick up the rebuilt
plugin bundles from #3878. 13.24.0 shipped manifests that claimed 13.24.0
while plugin/scripts/*.cjs still carried 13.23.1 bytes; the artifacts were
corrected in place, but existing 13.24.0 installs have no version change to
trigger an upgrade. 13.24.1 gives them one.
Version bumped across all 11 manifests and the README badge, then
regenerated via `npm run build-and-sync`. All four .cjs bundles are
byte-identical to 13.24.0 after normalizing the version string -- no source
changed since 4da9ffc6, so this is a pure version delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fu9bpdG4AWZNq3m8zkUkYg
* fix: rebuild plugin bundles so committed artifacts match manifest 13.24.0
The 13.24.0 release commit (85ccd626) bumped the manifests and CHANGELOG
but never re-ran the build, so plugin/scripts/*.cjs kept the 13.23.1 bytes
last produced by 89ca057a. The Claude Code marketplace installs straight
from this repo (.claude-plugin/marketplace.json -> "source": "./plugin"),
so every marketplace user on 13.24.0 has been executing 13.23.1 code.
ensureWorkerRunning() compares the resolved plugin version (13.24.0, taken
from the plugin cache directory name) against the worker's baked-in
__DEFAULT_PACKAGE_VERSION__ (13.23.1, reported by /api/health). The
mismatch SIGKILLs the worker and respawns the same stale file on every
hook event, with no state that survives the hook process to bound it --
an unbounded kill/respawn loop that takes the in-flight observer
generator down with it, so no observations get written.
This is a genuine `npm run build`, not a version-string patch. The
bundles were stale in code, not merely in the constant: src/ moved 704
insertions across 12 files since 13.23.1, including the observer's
<skip_summary reason="noise" /> protocol change, the new manual-session
module, and the platform_source plumbing in SessionStore and MemoryRoutes.
No version bump: the manifests were already correct at 13.24.0. It is the
artifacts that were wrong.
Fixes#3857
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bshprs1vjut2XmaGTRESqX
* fix(tests): restore module mocks so worker-spawner stubs stop leaking
bun runs the whole suite in one process and mock.module is process-global
and sticky, so stubs installed by one test file stay installed for every
file loaded after it.
tests/services/worker-spawner.test.ts mocked
src/services/infrastructure/{ProcessManager,HealthMonitor}.js and never
restored them. tests/infrastructure/{health-monitor,process-manager}.test.ts
import the same symbols through the src/services/infrastructure/index.js
barrel, so they silently exercised those stubs instead of the real code:
isPortInUse returned false without touching net.createServer, waitForHealth
returned false without fetching, getPlatformTimeout skipped the Windows
doubling, cleanStalePidFile always reported 'dead', and spawnDaemon never
returned undefined. waitForPortFree stayed real but resolved through the
stubbed isPortInUse binding, inverting its timeout case.
This is order-dependent, not new: bun walks test files in filesystem order,
and a fresh CI checkout loads worker-spawner (59) well before health-monitor
(169) and process-manager (172), while many local checkouts load
tests/infrastructure first and pass. That ordering is what surfaced 19
failures in Actions run 33939805959.
Snapshot the real namespaces eagerly, before the mock.module calls, and
reinstall them in afterAll. The snapshot must be eager: `import * as x`
yields a live namespace object that bun re-points when the module is mocked,
so spreading it inside afterAll copies the stubs back in. That is exactly
the latent bug in tests/cli/handlers/context-session-start.test.ts, whose
restore was re-installing its own hook-settings, oauth-token, project-name
and worker-utils stubs; fixed here the same way.
Tests only. No source, plugin bundle, or version changes — the committed
artifacts still match manifest 13.24.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcjsK6QqeBcYXbJ8gqBsea
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Catch GitHub up to the already-published npm 13.24.0. Updates every version
manifest and adds the 13.24.0 changelog for PR #3842.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
The gateway fallback said "allow one normal gateway request as a probe", but
nothing made it one. shouldUseCmemFallback is a subtraction and a less-than,
and ensureGeneratorRunning gates per session, so 28-69 live sessions are 28-69
independent callers.
It is worse than the in-memory case. The marker lives on disk
(CLAUDE_MEM_PRO_FALLBACK_AT in settings.json, loaded uncached), so every
process parses the same ISO string and computes the same expiry instant -- the
herd was handed a shared clock to synchronize on. Each member's failure then
does a read-modify-write of the user's whole settings.json to re-arm the
marker; the write is atomic but the sequence is not, so concurrent settings
edits can be clobbered.
The re-probe is now claimed through the breaker's existing machinery under a
DISTINCT key, 'cmem-gateway'. Distinct because tryAdmitQuotaProbe takes the
cooldown per call: this path's 15 minutes against the provider breaker's 30
would otherwise let two callers reach contradictory answers about whether the
same breaker is armed, and provider switches (which this branch makes routine)
make the collision reachable in both directions -- a fresh funded key born into
an armed breaker and withheld for a refusal it never earned.
getSelectedProvider stays read-only for diagnostics and status; only the new
selectProviderForGenerator claims. Every path out of ensureGeneratorRunning
that does not actually send releases the claim, including the one where a
generator is already running, so a claim cannot outlive the call that took it.
Also regenerates the plugin bundles rather than hand-merging them, which is
what the five .cjs conflicts in this consolidation always needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWmJDjFM6BNmGaLJ7Xbrsy
Both from greptile review on #3820, both reproduced against a live worker.
Clickjacking: requiring a real click stops a drive-by <img>, but not an
attacker who frames the page and harvests that click through an overlay — the
code comment claimed otherwise. The route now refuses to be framed at all:
Content-Security-Policy: frame-ancestors 'none' plus X-Frame-Options: DENY.
Health poll accepted the dying worker: the page treated any ok /health as
"restarted", but runShutdownSequence waits out a graceful deadline before it
exits and nothing closes the listener first, so the outgoing worker keeps
answering well past the first 500ms poll. Measured across a real restart:
t=500ms health OK, pid=6976 <- the worker we just asked to die
t=1000ms+ no response
(successor then spawns)
/health now reports pid, the page bakes in the pid of the worker that served
it, and success requires a DIFFERENT pid. A worker too old to report one falls
back to the previous "any healthy response" behavior rather than hanging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>