Skip to content

feat(claude-code): SessionStart resolves --space from cwd#224

Merged
wey-gu merged 5 commits into
nowledge-co:mainfrom
wuxianzhong-gpt:feat/session-start-space-from-cwd
May 11, 2026
Merged

feat(claude-code): SessionStart resolves --space from cwd#224
wey-gu merged 5 commits into
nowledge-co:mainfrom
wuxianzhong-gpt:feat/session-start-space-from-cwd

Conversation

@wuxianzhong-gpt

@wuxianzhong-gpt wuxianzhong-gpt commented May 6, 2026

Copy link
Copy Markdown

Why

The SessionStart hook in nowledge-mem-claude-code-plugin calls nmem wm read unconditionally — 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-center session, hijacking the model's first reply with stale, off-project advice).

What

Two SessionStart command strings (the startup|resume|clear matcher and the compact matcher) are updated to resolve a per-project space name and try it first.

Resolution order:

  1. $NMEM_SPACE — explicit shell override
  2. basename "$(dirname "$(git rev-parse --git-common-dir)")" | tr '[:upper:]' '[:lower:]' — the main-repo directory name; --git-common-dir makes worktrees share the main repo's lane
  3. empty (non-git cwd → no space resolution)

Behavior with the resolved name:

  1. If $SPACE is non-empty, run nmem wm read --space "$SPACE" and inject only when the API reports exists: true with non-empty content.
  2. Otherwise, or on empty result, fall back to the legacy default-space nmem wm read.
  3. Otherwise, the existing ~/ai-now/memory.md fallback.

This aligns Claude Code's SessionStart with the Space-aware execution guidance in docs/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:

nmem spaces enable
nmem spaces create "<your-repo-basename>" --icon code
nmem wm edit --space "<your-repo-basename>" -m "## Focus Areas\n- ..."

Override per shell: export NMEM_SPACE=....

Tested

Manually exercised against nmem 0.x with three workspaces:

  • ✅ Worktree under agent-center/.claude/worktrees/<name>SPACE=agent-center (worktrees share main repo's lane via --git-common-dir)
  • ✅ Project space exists with content → injected
  • ✅ Project space exists but Working Memory empty → falls back to default-space read
  • ✅ Project space does not exist → falls back to default-space read
  • cd /tmp (non-git) → space resolution short-circuits, default-space read runs
  • python3 -m json.tool hooks.json passes after edits

Notes

  • The resolution is intentionally cwd-derived rather than reading a user config file, so it works out-of-the-box with no additional setup beyond what users already do (spaces create <repo>).
  • Open to gating behind a config flag (NMEM_SPACE_FROM_CWD=0 to 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.
  • Bumps plugin.json version from 0.7.7 → 0.7.8. Did not touch root marketplace.json (its nowledge-mem entry 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

    • Per-project, space-aware working memory resolution with configurable override
    • Post-compaction reminder to guide saving insights
  • Bug Fixes

    • More reliable memory retrieval with robust fallbacks
    • Improved Windows command/path compatibility
  • Version

    • Plugin bumped to 0.7.8

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>
@coderabbitai

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
nowledge-mem-claude-code-plugin/CHANGELOG.md (1)

8-19: 💤 Low value

LGTM — 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.json does not actually fall back to "empty" in non-git directories — see the comment on hooks/hooks.json line 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 win

Consider 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|clear matcher (line 10) and the compact matcher (line 19) — only the trailing post-compaction printf differs. Any future fix (including the non-git resolution fix flagged above) has to be made in two places and kept in sync.

Existing precedent: PreCompact and Stop already delegate to ${CLAUDE_PLUGIN_ROOT}/scripts/nmem-hook-save.py (lines 39 and 51). A parallel scripts/nmem-hook-read.sh would let both SessionStart matchers 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.json readable.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between eb8e303 and d303af6.

📒 Files selected for processing (3)
  • nowledge-mem-claude-code-plugin/.claude-plugin/plugin.json
  • nowledge-mem-claude-code-plugin/CHANGELOG.md
  • nowledge-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>
@wuxianzhong-gpt

Copy link
Copy Markdown
Author

Update — extending PR to also tag captured threads with --space

Pushed a follow-up commit (541fe60) extending the PR to the Stop / PreCompact thread-capture hook as well. The reasoning:

While verifying the SessionStart change against my own multi-project setup, I noticed the desktop app already maintains a per-space daily Working Memory file at ~/ai-now/spaces/sp_<key>_<id>/memory.md — so the per-space distill machinery is already there. But the existing Stop and PreCompact hooks call nmem t save --from claude-code without --space, so every captured Claude Code thread lands in the default space, and the per-space distill never has any source material to route per-project. The result is that even with spaces enabled and nmem wm read --space <repo> working from the SessionStart side, the daily Working Memory written back to disk by the desktop agent collapses every project's activity into the global ~/ai-now/memory.md, undoing the isolation.

The follow-up commit teaches nmem-hook-save.py to resolve --space using the same $NMEM_SPACEgit-common-dir basename → None chain as SessionStart, and append it to the save command when resolved. Same backward-compatibility profile: users who haven't created a matching space see no behavior change.

Updated PR scope

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_cwd returns agent-center for the main repo path and for a worktree under .claude/worktrees/<name> (worktrees share the main repo's lane via --git-common-dir)
  • ✅ Returns None for /tmp and $HOME (non-git cwds → no tag, save command unchanged)
  • NMEM_SPACE env 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.

@wey-gu wey-gu merged commit e3406bd into nowledge-co:main May 11, 2026
1 check passed
@wey-gu

wey-gu commented May 11, 2026

Copy link
Copy Markdown
Member

@wuxianzhong-gpt THANKS!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants