feat(claude-code): SessionStart resolves --space from cwd#224
Conversation
The default hook calls `nmem wm read` against the global default space, so Working Memory from project A leaks into Claude Code sessions opened in project B whenever the user maintains memory for multiple repos. SessionStart now resolves a per-project space name as `$NMEM_SPACE` → lowercased basename of the directory that holds `git rev-parse --git-common-dir` (so worktrees share the main repo's lane) → empty. It tries `nmem wm read --space "$SPACE"` first, accepting the response only when the API reports `exists: true` with non-empty content, and falls back to the legacy default-space `nmem wm read` + `~/ai-now/memory.md` chain otherwise. The same resolution is applied to the `compact` matcher. Backward compatible: users who have not enabled spaces or created a matching space see no behavior change. Aligns Claude Code's SessionStart with "Space-aware execution" in docs/PLUGIN_DEVELOPMENT_GUIDE.md — when the host has a real ambient lane (cwd + git repo), pass it through. Bumps plugin to 0.7.8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
nowledge-mem-claude-code-plugin/CHANGELOG.md (1)
8-19: 💤 Low valueLGTM — clear, well-structured entry.
Resolution order, fallback chain, backward-compat note, opt-in steps, and override mechanism are all documented. Note: the entry is dated 2026-05-07 (tomorrow); fine if this matches the planned release date.
Note: the implementation in
hooks.jsondoes not actually fall back to "empty" in non-git directories — see the comment onhooks/hooks.jsonline 10 for details. Either the implementation or this section should be reconciled.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@nowledge-mem-claude-code-plugin/CHANGELOG.md` around lines 8 - 19, The changelog claims SessionStart resolution falls back to "empty" in non-git directories but the actual implementation in hooks.json (see the SessionStart hook and the compact matcher entries) does not perform an "empty" fallback; reconcile by either updating hooks.json to implement an explicit empty fallback path when neither NMEM_SPACE nor a repo-derived space is found (add the conditional branch that sets --space to empty and adjust the Working Memory load logic accordingly) or by editing the CHANGELOG.md entry to remove the "→ empty" step and explicitly document the real behavior described in hooks/hooks.json (no empty fallback in non-git dirs and fallback to default-space nmem wm read), and ensure both SessionStart and compact matcher wording match the chosen implementation.nowledge-mem-claude-code-plugin/hooks/hooks.json (1)
10-19: ⚡ Quick winConsider extracting the shared prelude to
scripts/nmem-hook-read.sh.The entire nmem-detection + Windows bridge + Python lookup + SPACE resolution + dispatch + fallback chain is duplicated byte-for-byte across the
startup|resume|clearmatcher (line 10) and thecompactmatcher (line 19) — only the trailing post-compactionprintfdiffers. Any future fix (including the non-git resolution fix flagged above) has to be made in two places and kept in sync.Existing precedent:
PreCompactandStopalready delegate to${CLAUDE_PLUGIN_ROOT}/scripts/nmem-hook-save.py(lines 39 and 51). A parallelscripts/nmem-hook-read.shwould let bothSessionStartmatchers shrink to:"command": "sh \"${CLAUDE_PLUGIN_ROOT:-}/scripts/nmem-hook-read.sh\"" // and for compact: "command": "sh \"${CLAUDE_PLUGIN_ROOT:-}/scripts/nmem-hook-read.sh\" && printf '\\n---\\nContext was compacted...'"Single source of truth for the resolution rules, much easier to review/test, and keeps
hooks.jsonreadable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@nowledge-mem-claude-code-plugin/hooks/hooks.json` around lines 10 - 19, Duplicate nmem detection/Windows-bridge/PY+SPACE resolution and fallback logic currently embedded in the "startup"/"resume"/"clear" matcher command and repeated in the "compact" matcher should be extracted into a single shell script (scripts/nmem-hook-read.sh); implement the full nmem lookup and fallback chain inside that script (preserving the nmem/.cmd wrapper, PY resolution, SPACE computation, JSON parsing behavior and file fallback), then replace the inline command in the matchers (the long "command": "...") with a call to sh "${CLAUDE_PLUGIN_ROOT:-}/scripts/nmem-hook-read.sh" and in the "compact" matcher append the existing printf only after invoking that script (i.e. sh ".../nmem-hook-read.sh" && printf '...'), following the same delegation pattern used by PreCompact/Stop -> scripts/nmem-hook-save.py.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@nowledge-mem-claude-code-plugin/CHANGELOG.md`:
- Around line 8-19: The changelog claims SessionStart resolution falls back to
"empty" in non-git directories but the actual implementation in hooks.json (see
the SessionStart hook and the compact matcher entries) does not perform an
"empty" fallback; reconcile by either updating hooks.json to implement an
explicit empty fallback path when neither NMEM_SPACE nor a repo-derived space is
found (add the conditional branch that sets --space to empty and adjust the
Working Memory load logic accordingly) or by editing the CHANGELOG.md entry to
remove the "→ empty" step and explicitly document the real behavior described in
hooks/hooks.json (no empty fallback in non-git dirs and fallback to
default-space nmem wm read), and ensure both SessionStart and compact matcher
wording match the chosen implementation.
In `@nowledge-mem-claude-code-plugin/hooks/hooks.json`:
- Around line 10-19: Duplicate nmem detection/Windows-bridge/PY+SPACE resolution
and fallback logic currently embedded in the "startup"/"resume"/"clear" matcher
command and repeated in the "compact" matcher should be extracted into a single
shell script (scripts/nmem-hook-read.sh); implement the full nmem lookup and
fallback chain inside that script (preserving the nmem/.cmd wrapper, PY
resolution, SPACE computation, JSON parsing behavior and file fallback), then
replace the inline command in the matchers (the long "command": "...") with a
call to sh "${CLAUDE_PLUGIN_ROOT:-}/scripts/nmem-hook-read.sh" and in the
"compact" matcher append the existing printf only after invoking that script
(i.e. sh ".../nmem-hook-read.sh" && printf '...'), following the same delegation
pattern used by PreCompact/Stop -> scripts/nmem-hook-save.py.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3429ff3e-83a7-4d9d-9035-22e0cd377308
📒 Files selected for processing (3)
nowledge-mem-claude-code-plugin/.claude-plugin/plugin.jsonnowledge-mem-claude-code-plugin/CHANGELOG.mdnowledge-mem-claude-code-plugin/hooks/hooks.json
The Stop and PreCompact hooks call `nmem t save --from claude-code` without `--space`, so every captured Claude Code thread lands in the default space. The desktop app already has per-space daily distill machinery (e.g. it writes per-space briefings to `~/ai-now/spaces/sp_<key>_<id>/memory.md` when threads carry a space tag), but with no tag at save time the distill collapses every project's activity back into the global default `~/ai-now/memory.md`. This commit teaches `nmem-hook-save.py` to resolve a per-project space the same way the SessionStart hook does — `$NMEM_SPACE` env override, then lowercased basename of the directory holding `git rev-parse --git-common-dir` (so worktrees share the main repo's lane), then `None` for non-git cwds. When resolved, `--space "$SPACE"` is appended to the save command. When `None`, the save command is unchanged. Backward compatible: behavior is identical for users who have not enabled spaces or have not created a space whose name matches their repo basename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update — extending PR to also tag captured threads with
|
| File | Change |
|---|---|
hooks/hooks.json |
SessionStart startup|resume|clear + compact matchers resolve --space from cwd |
scripts/nmem-hook-save.py |
Stop / PreCompact thread save tags --space from cwd |
CHANGELOG.md |
0.7.8 entry now covers both changes |
.claude-plugin/plugin.json |
0.7.7 → 0.7.8 |
+66 / -3 across 4 files now. Stats: 2 commits, both reviewable independently.
Tested locally
- ✅
_resolve_space_from_cwdreturnsagent-centerfor the main repo path and for a worktree under.claude/worktrees/<name>(worktrees share the main repo's lane via--git-common-dir) - ✅ Returns
Nonefor/tmpand$HOME(non-git cwds → no tag, save command unchanged) - ✅
NMEM_SPACEenv override takes precedence - ✅
python3 -c "import ast; ast.parse(open('nmem-hook-save.py').read())"passes
Happy to split this into two PRs if you'd prefer keeping each hook surface separate — just say the word.
|
@wuxianzhong-gpt THANKS! |
Why
The
SessionStarthook innowledge-mem-claude-code-plugincallsnmem wm readunconditionally — it never passes--space, so Working Memory always comes from the global default space.For users who maintain memory across multiple Git projects, this means the same Working Memory gets injected into every Claude Code session, regardless of which repo Claude was opened in. Context from project A bleeds into a session opened in project B (real example: a PipeAgent merge-conflict briefing landing in an unrelated
agent-centersession, hijacking the model's first reply with stale, off-project advice).What
Two
SessionStartcommandstrings (thestartup|resume|clearmatcher and thecompactmatcher) are updated to resolve a per-project space name and try it first.Resolution order:
$NMEM_SPACE— explicit shell overridebasename "$(dirname "$(git rev-parse --git-common-dir)")" | tr '[:upper:]' '[:lower:]'— the main-repo directory name;--git-common-dirmakes worktrees share the main repo's laneBehavior with the resolved name:
$SPACEis non-empty, runnmem wm read --space "$SPACE"and inject only when the API reportsexists: truewith non-emptycontent.nmem wm read.~/ai-now/memory.mdfallback.This aligns Claude Code's
SessionStartwith the Space-aware execution guidance indocs/PLUGIN_DEVELOPMENT_GUIDE.md(lines 32–41): when the host has a real ambient lane, pass it through; otherwise use the default space silently. Claude Code's session cwd + git context is that lane.Backward compatibility
Strictly additive. If the user has not run
nmem spaces enable, or has not created a space whose name matches their repo basename, the project-space lookup misses, the hook drops to the existing default-space path, and behavior is unchanged.To opt in:
Override per shell:
export NMEM_SPACE=....Tested
Manually exercised against
nmem0.x with three workspaces:agent-center/.claude/worktrees/<name>→SPACE=agent-center(worktrees share main repo's lane via--git-common-dir)cd /tmp(non-git) → space resolution short-circuits, default-space read runspython3 -m json.tool hooks.jsonpasses after editsNotes
spaces create <repo>).NMEM_SPACE_FROM_CWD=0to disable) if you'd prefer the new behavior be opt-in. As written, the fallbacks make it functionally opt-in already — nothing changes until the user creates a matching space.plugin.jsonversion from 0.7.7 → 0.7.8. Did not touch rootmarketplace.json(itsnowledge-mementry currently lags at 0.7.5; happy to sync in a follow-up or in this PR if you'd prefer).🤖 Drafted with Claude Code — happy to iterate based on review feedback.
Summary by CodeRabbit
New Features
Bug Fixes
Version