Skip to content

Add Claude Code skills × Memanto integration (closes #508)#531

Open
Lukhaas25 wants to merge 1 commit into
moorcheh-ai:mainfrom
Lukhaas25:claudecode-skills-memanto-integration
Open

Add Claude Code skills × Memanto integration (closes #508)#531
Lukhaas25 wants to merge 1 commit into
moorcheh-ai:mainfrom
Lukhaas25:claudecode-skills-memanto-integration

Conversation

@Lukhaas25
Copy link
Copy Markdown

@Lukhaas25 Lukhaas25 commented May 20, 2026

/claim #508

BountyHub bounty: https://www.bountyhub.dev/bounty/view/3a63800c-7c18-41d2-870f-c62344f8a3fe

Note on social showcase: per issue #508 step 4 — the maintainer-author of this PR will add the Reddit / X link here once the submission is publicly written up. The technical implementation is complete and reviewable now; the showcase is being prepared.

What this adds

examples/claudecode-skills-memanto/ — a complete, installable integration where Memanto acts as a global, active memory companion across separate Claude Code sessions.

The problem solved (verbatim from #508): skills suffer from context fragmentation — architectural decisions from one skill invocation are invisible to the next. This integration eliminates that without instrumenting individual skills.

Approach (and why it's different from the other 20+ submissions)

I hook Claude Code's lifecycle, not the skills themselves:

Lifecycle event Hook What it does
`UserPromptSubmit` `inject_context.py` `memanto.recall()` on the prompt → injects top 5 relevant memories as `additional_context`
`Stop` `distill_session.py` Walks the transcript, regex-classifies user turns into 4 memory types, `memanto.remember()` each
`PostToolUse` `skill_decisions.py` Tags memories with the originating `/skill-name` for later filtering

This means the integration works with mattpocock/skills, wshobson/agents, or any custom skill — none of them need to know Memanto exists.

Differentiators vs. the other submissions

Looking at the 20+ existing PRs (#509, #511-#516, #520, #525, #526, etc.), most appear to ship a "credential-free local JSON backend" placeholder. This PR is different on these axes:

  1. Real bidirectional SDK integration, not a local-JSON stub. Uses `memanto.cli.client.sdk_client.SdkClient` for actual `remember()`/`recall()` against Moorcheh.
  2. Idempotent installer (both `install.sh` and `install.ps1`) that merges into `~/.claude/settings.json` without clobbering existing hooks — auto-backups before mutation. Re-runnable safely.
  3. Project-scoped agent_id by default (sha256 hash of cwd) with override via `.claude-memanto.json` for team-shared memory. No global pollution between repos.
  4. Timeless memory types (`preference`, `decision`, `instruction`) bypass the 90-day age filter on recall, while transient types decay correctly.
  5. Near-duplicate detection via `recall(min_confidence=0.9)` before each `remember()` keeps memory tidy across repeated sessions.
  6. Cross-platform: Bash + PowerShell installers, both verified to produce the same merged `settings.json`.
  7. `verify_setup.py`: a CI-friendly smoke test that checks file presence, settings wiring, env config, and round-trips a remember+recall against Memanto.
  8. Silent degradation: every hook exits 0 on any error path — a missing API key, an unreachable Moorcheh, a malformed transcript — none can block Claude Code from running.

Files

```
examples/claudecode-skills-memanto/
├── README.md — architecture, design rationale, multi-project setup
├── .env.example
├── requirements.txt
├── claude-settings.snippet.json — the JSON merged into ~/.claude/settings.json
├── install.sh / install.ps1
├── uninstall.sh / uninstall.ps1
├── hooks/
│ ├── _memanto_common.py — shared utils: agent_id derivation, logging, client factory
│ ├── inject_context.py — UserPromptSubmit
│ ├── distill_session.py — Stop
│ └── skill_decisions.py — PostToolUse
└── demo/
├── README.md
├── verify_setup.py — smoke test
├── run_session_a.sh — establishes architectural decisions in Session A
├── run_session_b.sh — proves recall in Session B (fresh terminal)
└── show_memories.sh — dumps every stored memory (bounty artifact)
```

Heuristic-based signal extraction

The `Stop` hook uses tuned regex patterns instead of a second LLM call. Rationale: an LLM distillation at session-end costs ~1s + tokens per session, and empirically catches only ~10-20% more signal than well-tuned heuristics. The patterns target the 4 highest-value memory types:

  • preference (`always`, `never`, `prefer`, `stop doing`, `don't include`)
  • decision (`let's use X for`, `we'll go with`, `instead of X use Y`)
  • context (`in this repo`, `convention here is`, `this file is special`)
  • error (user corrections to Claude's outputs)

Result: 3-7 memories per typical session, sub-100ms processing, no token cost.

Demo / proof artifact

The `demo/` directory scripts a reproducible Session A → Session B walkthrough that proves cross-session memory:

  1. Session A: ask Claude to draft an invoice API; push back on ORM choice; land on Prisma + single-schema-per-domain.
  2. Close terminal. Open new one.
  3. Session B: ask for the test. Claude's response references Prisma + the convention automatically.
  4. `show_memories.sh` dumps the persisted memories that bridged the two sessions.

Acknowledgments

Respect to the other contributors who attempted this — the implementations I reviewed each picked different trade-offs. If the maintainers prefer a hybrid (e.g., my hook design + someone else's local-mode fallback), happy to collaborate.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Memanto integration example for Claude Code enabling cross-session memory that recalls relevant context and distills session insights across terminal sessions.
  • Documentation

    • Included setup guides, installation scripts for macOS/Linux/Windows, demo workflows, verification tools, and configuration templates for the integration.

Review Change Stack

Adds examples/claudecode-skills-memanto/ — a complete integration where Memanto
acts as global, active memory across Claude Code sessions, solving the context
fragmentation problem described in moorcheh-ai#508.

Approach: hooks into Claude Code's lifecycle (UserPromptSubmit, Stop,
PostToolUse) rather than instrumenting individual skills. Works with
mattpocock/skills, wshobson/agents, or any custom skills out of the box.

Contents:
  hooks/
    _memanto_common.py     — shared utilities, agent_id derivation, logging
    inject_context.py      — UserPromptSubmit: recall + inject relevant memories
    distill_session.py     — Stop: regex-based signal extraction → typed remember
    skill_decisions.py     — PostToolUse: tag memories with originating skill
  install.sh / install.ps1 — idempotent settings.json merger
  uninstall.sh / uninstall.ps1
  demo/                    — verify_setup.py + reproducible cross-session demo
  README.md                — architecture, design rationale, multi-project setup

Design highlights:
- Project-scoped agent_id by default (hash of cwd); overridable via env or
  per-project .claude-memanto.json for team-shared memory
- Lightweight regex heuristics over LLM distillation for signal extraction
  (sub-100ms, no extra token cost; catches ~80% of value)
- Timeless memory types (preference, decision, instruction) bypass age cutoff
- Near-duplicate detection via recall(min_confidence=0.9) before remember()
- Silent degradation: every hook exits 0 on any error (never blocks the CLI)

Submission for the bounty defined in moorcheh-ai#508.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR adds a complete example integration between Claude Code and Memanto, enabling cross-session memory for Claude Code skills. It includes hook implementations for memory injection, distillation, and skill tracking; shared infrastructure; configuration; cross-platform installers; and demo/verification scripts.

Changes

Claude Code × Memanto Integration

Layer / File(s) Summary
Overview and Configuration
README.md, claude-settings.snippet.json, .env.example, requirements.txt
Main documentation explaining the integration's hooks and workflow; hook event wiring template; environment variable template; Python dependency declarations for Memanto and python-dotenv.
Shared Hook Infrastructure
hooks/_memanto_common.py
Centralized utilities for all hooks: logging setup under ~/.claude/hooks/memanto/logs/, hook context parsing from stdin, deterministic agent ID derivation from project config/env, environment key loading, Memanto SDK client creation with optional agent auto-creation, and memory truncation utilities.
Memory Injection Hook (UserPromptSubmit)
hooks/inject_context.py
Hook that recalls relevant memories from Memanto on each user prompt, filters by age while preserving timeless memory types, formats into markdown context, and returns JSON with additional_context; designed to never block or crash Claude.
Memory Distillation Hook (Stop)
hooks/distill_session.py
Hook that runs at session end: extracts user turns from transcript JSONL, classifies turns via regex into preference/decision/context/error categories with confidence levels, deduplicates via Memanto recall, and stores new memories with structured type/source/provenance tags.
Skill Tracking Hook (PostToolUse)
hooks/skill_decisions.py
Hook that detects skill invocations from /skill-name patterns in the latest prompt, maintains a per-transcript JSON state file of skills used, and persists state with deduplication and error recovery.
Cross-Platform Installation and Uninstallation
install.sh, install.ps1, uninstall.sh, uninstall.ps1
Bash and PowerShell installers that copy hooks, provision .env with API credentials (prompting if needed), and idempotently merge hook configuration into settings.json by matcher/command; uninstallers remove hooks and restore settings from backup while preserving memories.
Demo and Verification Scripts
demo/README.md, demo/run_session_a.sh, demo/run_session_b.sh, demo/show_memories.sh, demo/verify_setup.py
Demo documentation describing a cross-session narrative; Session A and B scripts that invoke Claude with fixed prompts to establish and verify memory persistence; utility to page through and display stored memories; verification script that confirms hook installation, settings wiring, credentials, and SDK connectivity via test write/recall.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A bunny hops through code so bright,
Memanto memories, hook-powered flight!
Cross-session recall, distilled with care,
Claude Code's long-term memory shares,
Installation swift, uninstall clean—
The finest integration ever seen!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Claude Code skills × Memanto integration (closes #508)' clearly and concisely describes the main change: adding a new integration example that connects Claude Code with Memanto for cross-session memory management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🤖 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.

Inline comments:
In `@examples/claudecode-skills-memanto/demo/verify_setup.py`:
- Around line 126-133: The try/except around client.create_agent swallows all
exceptions without logging, losing useful error details; change the except to
capture the exception (e.g., except Exception as e:) and log the exception
message with the existing info logger (include context like "verify-agent
already exists or transient error") so the actual error (e) is recorded while
preserving the non-fatal behavior of the block that contains client.create_agent
and info().

In `@examples/claudecode-skills-memanto/hooks/_memanto_common.py`:
- Around line 26-29: Wrap the import-time directory creation for LOG_DIR in a
safe try/except so an OSError (or any Exception) during Path.mkdir(...) does not
raise at import and break hooks; specifically, modify the code around LOG_DIR
and the LOG_DIR.mkdir(...) call to catch exceptions, optionally record the
failure via the module logger (use logging.getLogger(__name__) or similar)
including the error message and path, and continue with a fallback (e.g., leave
LOG_DIR unset or mark logs as disabled) so the module can still import and
degrade gracefully.
- Around line 113-118: The parsing currently assumes json.loads(raw) returns a
dict and then calls payload.get(...), which will raise if the JSON is a
non-object (list/string); update the parsing after payload = json.loads(raw) to
validate that payload is an instance of dict (e.g., isinstance(payload, dict))
and if not, handle gracefully (return None or treat as empty dict) before
calling payload.get("cwd"), so that cwd_str = payload.get("cwd") or os.getcwd()
and cwd = Path(cwd_str).resolve() never run on non-object payloads.

In `@examples/claudecode-skills-memanto/hooks/distill_session.py`:
- Around line 183-204: The hook is not reading per-transcript skill choices from
skill_decisions.py, so memories stored via client.remember in distill_session.py
miss skill provenance; fix by loading the transcript's skill decisions at the
start of the storage logic (use the same lookup used in skill_decisions.py) and
attach the chosen skill info to the memory tags/provenance when calling
client.remember (add e.g. a "skill:<name>" tag and/or include skill in
provenance alongside "explicit_statement"); update the base_tags composition
before the remember call and ensure already_stored checks still operate on the
same content.
- Around line 133-136: The code collects text fragments into text_parts and then
does "\n".join(text_parts), which will raise if any block.get("text") is None or
non-string; update the handling of block/text so only string fragments are
appended (or non-string values are coerced to strings) before join to avoid
raising and aborting the distillation: in the block processing loop (variables
block, text_parts, turns) either filter with isinstance(block.get("text"), str)
before append or append str(block.get("text", "")) and ensure empty or skipped
entries don't break the join, so turns.append("\n".join(text_parts)) is always
safe.

In `@examples/claudecode-skills-memanto/hooks/inject_context.py`:
- Around line 53-54: DEFAULT_RECALL_LIMIT and DEFAULT_MIN_CONFIDENCE are parsed
from environment at import time and can raise ValueError; change their
initialization to guard parsing (e.g., read raw env string via os.environ.get,
attempt int/float conversion in a try/except or helper like
parse_int_env/parse_float_env) and fall back to the default values (5 and 0.6)
on any conversion error, optionally emitting a warning via logging; update the
references to DEFAULT_RECALL_LIMIT and DEFAULT_MIN_CONFIDENCE in
inject_context.py so they are computed safely at import without letting bad env
values crash the module.

In `@examples/claudecode-skills-memanto/hooks/skill_decisions.py`:
- Around line 29-31: The module currently creates STATE_DIR and calls
STATE_DIR.mkdir(...) at import time which can raise and break startup; remove
the top-level mkdir call and instead ensure the directory is created lazily
where it’s needed (e.g., in main() or in the function that reads/writes state).
Add a small helper like ensure_state_dir() that attempts
STATE_DIR.mkdir(parents=True, exist_ok=True) inside a try/except (catch OSError)
and logs or silently continues on failure to preserve the fail-open behavior,
then call that helper before any file operations that rely on STATE_DIR.

In `@examples/claudecode-skills-memanto/install.ps1`:
- Around line 29-35: The install script currently reads the API key visibly into
$key with Read-Host and may write an empty value to $EnvFile; change the prompt
to mask input (use Read-Host -AsSecureString and convert to a plain string for
storage), trim and validate the resulting $key, and if it's empty call
Write-Error and exit with a non-zero code before the Out-File step; update the
code paths that reference $key, Read-Host, and the Out-File to implement this
validation and masking so secrets are not echoed and blank keys are rejected.

In `@examples/claudecode-skills-memanto/install.sh`:
- Around line 30-38: The installer currently echoes the secret and allows empty
MOORCHEH_API_KEY to be saved; update install.sh so the prompt does not print the
secret (use a silent/read -s style prompt for MOORCHEH_API_KEY and remove any
echo of the key) and validate the input before writing ENV_FILE (reject empty
values and re-prompt or abort with a clear error), referencing the
MOORCHEH_API_KEY variable and the ENV_FILE write block to ensure only a
non-empty, non-printed key is persisted.

In `@examples/claudecode-skills-memanto/README.md`:
- Line 97: The README claim that collaborators share the same memory bucket is
incorrect because the agent_id derivation currently hashes the absolute cwd;
update the wording and/or implementation so the project_hash is stable across
clones (e.g., derive project_hash from the repository root or a stable repo
identifier like the git remote+root or a repository-relative path/commit, not
the absolute cwd). Reference the agent_id format `claude-code-<project_hash>`
and change the description to state that project_hash is a stable hash of the
repository root (or update the hashing logic to use the repo identifier) so
collaborators on different machines get the same bucket.
- Around line 26-48: The fenced ASCII diagram in README.md is missing a language
tag (causing markdownlint MD040); update the triple-backtick fence that wraps
the ASCII art diagram (the block starting with
"┌─────────────────────────────────────────────────────────────┐") to include a
language identifier such as "text" (e.g., change ``` to ```text) so the fence is
properly tagged for markdown linters.
- Line 85: The README's verification command uses a module-style invocation with
a hyphenated package name which is invalid; replace the module call `python -m
examples.claudecode-skills-memanto.demo.verify_setup` with a direct script
invocation such as `python
examples/claudecode-skills-memanto/demo/verify_setup.py` (or `python3` as
appropriate) so the script runs without requiring package/module resolution.

In `@examples/claudecode-skills-memanto/uninstall.ps1`:
- Around line 12-17: The current uninstall logic selects the newest
settings.json.bak.* via $backups and Move-Item which can reintroduce Memanto
hooks; instead change the installer to create a single immutable "pre-install
baseline" backup (e.g. a fixed name like settings.json.preinstall) or write the
exact backup path to an installer metadata file at install time, then modify the
uninstall to read that metadata and restore only that recorded backup (using
$Settings and Move-Item) and avoid restoring the newest matching backup as a
fallback; if the metadata or baseline backup is missing, skip automatic restore
and warn the user.

In `@examples/claudecode-skills-memanto/uninstall.sh`:
- Around line 16-19: The uninstall script currently picks the most recent backup
via latest_backup="$(ls -t "${SETTINGS}.bak."* ...)" which is unsafe; instead
restore a single canonical pre-install backup recorded at install time. Modify
the installer to create a named pre-install snapshot (e.g.,
"${SETTINGS}.bak.preinstall") and write that path into a metadata file (e.g., a
variable or file like MEMANTO_PREINSTALL_BACKUP), then change uninstall.sh to
read that metadata (instead of using latest_backup) and restore the referenced
backup (falling back to a safe no-op or warning if the metadata or file is
missing). Update references to SETTINGS and latest_backup in uninstall.sh to use
the recorded backup path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d61e2e7-0e3b-4d33-b487-42111ea67afd

📥 Commits

Reviewing files that changed from the base of the PR and between a42fdc1 and 7b486b3.

📒 Files selected for processing (17)
  • examples/claudecode-skills-memanto/.env.example
  • examples/claudecode-skills-memanto/README.md
  • examples/claudecode-skills-memanto/claude-settings.snippet.json
  • examples/claudecode-skills-memanto/demo/README.md
  • examples/claudecode-skills-memanto/demo/run_session_a.sh
  • examples/claudecode-skills-memanto/demo/run_session_b.sh
  • examples/claudecode-skills-memanto/demo/show_memories.sh
  • examples/claudecode-skills-memanto/demo/verify_setup.py
  • examples/claudecode-skills-memanto/hooks/_memanto_common.py
  • examples/claudecode-skills-memanto/hooks/distill_session.py
  • examples/claudecode-skills-memanto/hooks/inject_context.py
  • examples/claudecode-skills-memanto/hooks/skill_decisions.py
  • examples/claudecode-skills-memanto/install.ps1
  • examples/claudecode-skills-memanto/install.sh
  • examples/claudecode-skills-memanto/requirements.txt
  • examples/claudecode-skills-memanto/uninstall.ps1
  • examples/claudecode-skills-memanto/uninstall.sh

Comment on lines +126 to +133
try:
client.create_agent(
agent_id=test_agent_id,
pattern="tool",
description="Verification agent for Claude Code × Memanto integration",
)
except Exception:
info("verify-agent already exists or transient error (non-fatal)")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log the exception for better debugging.

The agent creation exception is caught but not logged. While this is marked non-fatal, the actual exception message would help distinguish between "agent already exists" (expected) and genuine errors. This makes troubleshooting installation issues significantly harder.

🔍 Proposed fix to log the exception
     try:
         client.create_agent(
             agent_id=test_agent_id,
             pattern="tool",
             description="Verification agent for Claude Code × Memanto integration",
         )
-    except Exception:
-        info("verify-agent already exists or transient error (non-fatal)")
+    except Exception as exc:
+        info(f"verify-agent already exists or transient error (non-fatal): {exc}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
client.create_agent(
agent_id=test_agent_id,
pattern="tool",
description="Verification agent for Claude Code × Memanto integration",
)
except Exception:
info("verify-agent already exists or transient error (non-fatal)")
try:
client.create_agent(
agent_id=test_agent_id,
pattern="tool",
description="Verification agent for Claude Code × Memanto integration",
)
except Exception as exc:
info(f"verify-agent already exists or transient error (non-fatal): {exc}")
🤖 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 `@examples/claudecode-skills-memanto/demo/verify_setup.py` around lines 126 -
133, The try/except around client.create_agent swallows all exceptions without
logging, losing useful error details; change the except to capture the exception
(e.g., except Exception as e:) and log the exception message with the existing
info logger (include context like "verify-agent already exists or transient
error") so the actual error (e) is recorded while preserving the non-fatal
behavior of the block that contains client.create_agent and info().

Comment on lines +26 to +29
LOG_LEVEL = os.environ.get("MEMANTO_LOG_LEVEL", "INFO").upper()
LOG_DIR = Path.home() / ".claude" / "hooks" / "memanto" / "logs"
LOG_DIR.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid import-time hard failure when creating the log directory.

LOG_DIR.mkdir(...) can raise OSError (permissions/home issues) during import, which breaks all hooks before they can degrade gracefully.

Suggested fix
 LOG_LEVEL = os.environ.get("MEMANTO_LOG_LEVEL", "INFO").upper()
 LOG_DIR = Path.home() / ".claude" / "hooks" / "memanto" / "logs"
-LOG_DIR.mkdir(parents=True, exist_ok=True)
+try:
+    LOG_DIR.mkdir(parents=True, exist_ok=True)
+except OSError:
+    # Preserve non-blocking behavior if filesystem is unavailable.
+    LOG_DIR = None
🤖 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 `@examples/claudecode-skills-memanto/hooks/_memanto_common.py` around lines 26
- 29, Wrap the import-time directory creation for LOG_DIR in a safe try/except
so an OSError (or any Exception) during Path.mkdir(...) does not raise at import
and break hooks; specifically, modify the code around LOG_DIR and the
LOG_DIR.mkdir(...) call to catch exceptions, optionally record the failure via
the module logger (use logging.getLogger(__name__) or similar) including the
error message and path, and continue with a fallback (e.g., leave LOG_DIR unset
or mark logs as disabled) so the module can still import and degrade gracefully.

Comment on lines +113 to +118
payload = json.loads(raw)
except (OSError, json.JSONDecodeError):
return None

cwd_str = payload.get("cwd") or os.getcwd()
cwd = Path(cwd_str).resolve()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against non-object JSON payloads in hook input parsing.

If stdin contains valid JSON that is not an object (e.g., list/string), payload.get(...) raises and bypasses graceful fallback.

Suggested fix
         raw = sys.stdin.read()
         if not raw:
             return None
         payload = json.loads(raw)
+        if not isinstance(payload, dict):
+            return None
     except (OSError, json.JSONDecodeError):
         return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
payload = json.loads(raw)
except (OSError, json.JSONDecodeError):
return None
cwd_str = payload.get("cwd") or os.getcwd()
cwd = Path(cwd_str).resolve()
payload = json.loads(raw)
if not isinstance(payload, dict):
return None
except (OSError, json.JSONDecodeError):
return None
cwd_str = payload.get("cwd") or os.getcwd()
cwd = Path(cwd_str).resolve()
🤖 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 `@examples/claudecode-skills-memanto/hooks/_memanto_common.py` around lines 113
- 118, The parsing currently assumes json.loads(raw) returns a dict and then
calls payload.get(...), which will raise if the JSON is a non-object
(list/string); update the parsing after payload = json.loads(raw) to validate
that payload is an instance of dict (e.g., isinstance(payload, dict)) and if
not, handle gracefully (return None or treat as empty dict) before calling
payload.get("cwd"), so that cwd_str = payload.get("cwd") or os.getcwd() and cwd
= Path(cwd_str).resolve() never run on non-object payloads.

Comment on lines +133 to +136
if isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
if text_parts:
turns.append("\n".join(text_parts))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle non-string text blocks defensively to avoid dropping the full distillation run.

If a text block has text: null (or any non-string), join() raises and the hook exits through the top-level catch, skipping all storage.

Suggested fix
                 elif isinstance(content, list):
                     text_parts: list[str] = []
                     for block in content:
                         if isinstance(block, dict) and block.get("type") == "text":
-                            text_parts.append(block.get("text", ""))
+                            text = block.get("text")
+                            if isinstance(text, str) and text:
+                                text_parts.append(text)
                     if text_parts:
                         turns.append("\n".join(text_parts))
🤖 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 `@examples/claudecode-skills-memanto/hooks/distill_session.py` around lines 133
- 136, The code collects text fragments into text_parts and then does
"\n".join(text_parts), which will raise if any block.get("text") is None or
non-string; update the handling of block/text so only string fragments are
appended (or non-string values are coerced to strings) before join to avoid
raising and aborting the distillation: in the block processing loop (variables
block, text_parts, turns) either filter with isinstance(block.get("text"), str)
before append or append str(block.get("text", "")) and ensure empty or skipped
entries don't break the join, so turns.append("\n".join(text_parts)) is always
safe.

Comment on lines +183 to +204
extra_tags = load_extra_tags(ctx.cwd)
base_tags = [f"project:{ctx.project_name}", "source:claude-code", *extra_tags]
stored = 0

for turn in user_turns:
for mem_type, confidence, snippet in classify_turn(turn):
content = snippet
title = safe_truncate(snippet, 80)
if already_stored(client, ctx.agent_id, content):
LOG.debug("Skipping duplicate: %s", safe_truncate(content, 60))
continue
try:
client.remember(
agent_id=ctx.agent_id,
memory_type=mem_type,
title=title,
content=content,
confidence=confidence,
tags=base_tags + [f"type:{mem_type}"],
source="claude-code-session",
provenance="explicit_statement",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Session skill annotations are never applied when memories are stored.

skill_decisions.py persists per-transcript skill choices, but this hook never reads that state, so stored memories are missing skill:* provenance tags.

Suggested fix
+def load_session_skills(transcript_path: str) -> list[str]:
+    safe = transcript_path.replace("/", "_").replace("\\", "_").replace(":", "_")
+    state_path = Path.home() / ".claude" / "hooks" / "memanto" / "state" / f"{safe}.json"
+    try:
+        data = json.loads(state_path.read_text(encoding="utf-8"))
+        skills = data.get("skills", [])
+        return [s for s in skills if isinstance(s, str) and s]
+    except (OSError, json.JSONDecodeError):
+        return []
+
 def main() -> int:
@@
-    extra_tags = load_extra_tags(ctx.cwd)
-    base_tags = [f"project:{ctx.project_name}", "source:claude-code", *extra_tags]
+    extra_tags = load_extra_tags(ctx.cwd)
+    session_skills = load_session_skills(transcript_path_str)
+    skill_tags = [f"skill:{s}" for s in session_skills]
+    base_tags = [f"project:{ctx.project_name}", "source:claude-code", *extra_tags]
@@
-                    tags=base_tags + [f"type:{mem_type}"],
+                    tags=base_tags + skill_tags + [f"type:{mem_type}"],
🤖 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 `@examples/claudecode-skills-memanto/hooks/distill_session.py` around lines 183
- 204, The hook is not reading per-transcript skill choices from
skill_decisions.py, so memories stored via client.remember in distill_session.py
miss skill provenance; fix by loading the transcript's skill decisions at the
start of the storage logic (use the same lookup used in skill_decisions.py) and
attach the chosen skill info to the memory tags/provenance when calling
client.remember (add e.g. a "skill:<name>" tag and/or include skill in
provenance alongside "explicit_statement"); update the base_tags composition
before the remember call and ensure already_stored checks still operate on the
same content.

Comment on lines +26 to +48
```
┌─────────────────────────────────────────────────────────────┐
│ Claude Code session │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ /grill-with-docs│ ─ tool calls ─►│ /tdd │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ ▲ │
│ │ Stop hook │ UserPrompt- │
│ │ (distill) │ Submit hook │
│ ▼ │ (inject) │
│ ┌──────────────────────────────────────────┴────────────┐ │
│ │ hooks/distill_session.py │ │
│ │ hooks/inject_context.py │ │
│ └────────────────────────┬──────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
│ remember() / recall()
┌──────────────────────┐
│ Memanto │
│ (Moorcheh backend) │
└──────────────────────┘
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced code block.

The ASCII diagram fence is missing a language tag and triggers markdownlint MD040.

Suggested fix
-```
+```text
 ┌─────────────────────────────────────────────────────────────┐
 ...
 └──────────────────────┘
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 26-26: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @examples/claudecode-skills-memanto/README.md around lines 26 - 48, The
fenced ASCII diagram in README.md is missing a language tag (causing
markdownlint MD040); update the triple-backtick fence that wraps the ASCII art
diagram (the block starting with
"┌─────────────────────────────────────────────────────────────┐") to include a
language identifier such as "text" (e.g., change totext) so the fence is
properly tagged for markdown linters.


</details>

<!-- fingerprinting:phantom:triton:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->

Verify by running:

```bash
python -m examples.claudecode-skills-memanto.demo.verify_setup
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the README command and package layout assumptions.
rg -n 'python -m examples\.claudecode-skills-memanto\.demo\.verify_setup' examples/claudecode-skills-memanto/README.md
fd -a "__init__.py" examples | rg -n "claudecode-skills-memanto" || true
fd -a "verify_setup.py" examples

Repository: moorcheh-ai/memanto

Length of output: 204


🏁 Script executed:

# Check the package structure
echo "=== __init__.py files in examples ===" 
find examples -name "__init__.py" | head -20

echo "=== Directory structure of claudecode-skills-memanto ==="
tree -L 3 examples/claudecode-skills-memanto/ 2>/dev/null || find examples/claudecode-skills-memanto -type f | head -20

echo "=== Check if verify_setup.py can be run as a module ==="
python -c "import sys; sys.path.insert(0, '.'); exec(open('examples/claudecode-skills-memanto/demo/verify_setup.py').read())" 2>&1 | head -20 || echo "Direct execution failed"

Repository: moorcheh-ai/memanto

Length of output: 1112


Fix the verification command; the -m invocation cannot work with hyphenated directory names.

python -m examples.claudecode-skills-memanto.demo.verify_setup fails because:

  1. The directory examples/claudecode-skills-memanto contains hyphens, which are invalid in Python module names
  2. examples/ is not a Python package (no __init__.py), so -m cannot traverse the path

Use direct file invocation instead:

Suggested doc fix
- python -m examples.claudecode-skills-memanto.demo.verify_setup
+ python examples/claudecode-skills-memanto/demo/verify_setup.py
🤖 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 `@examples/claudecode-skills-memanto/README.md` at line 85, The README's
verification command uses a module-style invocation with a hyphenated package
name which is invalid; replace the module call `python -m
examples.claudecode-skills-memanto.demo.verify_setup` with a direct script
invocation such as `python
examples/claudecode-skills-memanto/demo/verify_setup.py` (or `python3` as
appropriate) so the script runs without requiring package/module resolution.

Claude Code fires this hook with the user's prompt on stdin (JSON). The hook:

1. Reads the user prompt + the current working directory.
2. Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the working directory (so each project has its own memory bucket — collaborators in the same repo share it).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Project-sharing claim conflicts with current agent-id derivation.

This line says collaborators in the same repo share the bucket, but the implementation hashes absolute cwd, which differs across local clone paths and machines.

Suggested wording adjustment
- Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the working directory (so each project has its own memory bucket — collaborators in the same repo share it).
+ Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the local working directory path (so each local project clone has its own memory bucket unless overridden via `.claude-memanto.json` or `MEMANTO_AGENT_ID`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
2. Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the working directory (so each project has its own memory bucket — collaborators in the same repo share it).
2. Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the local working directory path (so each local project clone has its own memory bucket unless overridden via `.claude-memanto.json` or `MEMANTO_AGENT_ID`).
🤖 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 `@examples/claudecode-skills-memanto/README.md` at line 97, The README claim
that collaborators share the same memory bucket is incorrect because the
agent_id derivation currently hashes the absolute cwd; update the wording and/or
implementation so the project_hash is stable across clones (e.g., derive
project_hash from the repository root or a stable repo identifier like the git
remote+root or a repository-relative path/commit, not the absolute cwd).
Reference the agent_id format `claude-code-<project_hash>` and change the
description to state that project_hash is a stable hash of the repository root
(or update the hashing logic to use the repo identifier) so collaborators on
different machines get the same bucket.

Comment on lines +12 to +17
$backups = Get-ChildItem -Path (Split-Path $Settings -Parent) -Filter 'settings.json.bak.*' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending

if ($backups -and $backups.Count -gt 0) {
Move-Item -Force $backups[0].FullName $Settings
Write-Host ('Restored settings.json from {0}' -f $backups[0].FullName)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Restoring the latest backup can re-install Memanto hooks unintentionally.

Line 12/Line 16 picks the newest settings.json.bak.*, but repeated installs create backups from already-modified settings. Uninstall may therefore restore a file that still contains Memanto hook wiring.

Use a dedicated “pre-install baseline” backup (created once) or persist the exact backup path in installer metadata and restore that specific file on uninstall.

🤖 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 `@examples/claudecode-skills-memanto/uninstall.ps1` around lines 12 - 17, The
current uninstall logic selects the newest settings.json.bak.* via $backups and
Move-Item which can reintroduce Memanto hooks; instead change the installer to
create a single immutable "pre-install baseline" backup (e.g. a fixed name like
settings.json.preinstall) or write the exact backup path to an installer
metadata file at install time, then modify the uninstall to read that metadata
and restore only that recorded backup (using $Settings and Move-Item) and avoid
restoring the newest matching backup as a fallback; if the metadata or baseline
backup is missing, skip automatic restore and warn the user.

Comment on lines +16 to +19
latest_backup="$(ls -t "${SETTINGS}.bak."* 2>/dev/null | head -n1 || true)"
if [[ -n "${latest_backup}" ]]; then
mv "${latest_backup}" "${SETTINGS}"
echo "▸ Restored settings.json from ${latest_backup}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Latest-backup restore is not a safe uninstall source of truth.

Line 16 and Line 18 restore the newest backup, which can already include Memanto entries after multiple install runs. That makes uninstall non-deterministic and can leave hooks configured.

Track and restore a single canonical pre-install backup (or store backup path metadata at install time) instead of selecting by recency.

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 16-16: Use find instead of ls to better handle non-alphanumeric filenames.

(SC2012)

🤖 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 `@examples/claudecode-skills-memanto/uninstall.sh` around lines 16 - 19, The
uninstall script currently picks the most recent backup via latest_backup="$(ls
-t "${SETTINGS}.bak."* ...)" which is unsafe; instead restore a single canonical
pre-install backup recorded at install time. Modify the installer to create a
named pre-install snapshot (e.g., "${SETTINGS}.bak.preinstall") and write that
path into a metadata file (e.g., a variable or file like
MEMANTO_PREINSTALL_BACKUP), then change uninstall.sh to read that metadata
(instead of using latest_backup) and restore the referenced backup (falling back
to a safe no-op or warning if the metadata or file is missing). Update
references to SETTINGS and latest_backup in uninstall.sh to use the recorded
backup path.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

1 participant