codebase-memory
Use the codebase knowledge graph for structural code queries, trace call paths and dependencies, inspect architecture, assess change impact, or activate the bundled Claude Code discovery hooks.
Install
npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/tools/codebase-memory
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
git clone https://github.com/ConnorGriffin/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole connorgriffin/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Codebase Memory
Follow reminder.md for the standing discovery policy; it is the one authored policy shared by this skill, the profile pointer, and the installed SessionStart hook.
When ensure reports unavailable, use its cbm-onboard diagnostic and bounded retry
sequence rather than guessing at a project or treating every failure as a sandbox
denial. The reminder owns the details.
Activate discovery guidance in Claude Code
After the standard skills installer copies this directory, run:
python3 ~/.claude/skills/codebase-memory/scripts/install.py
For a non-default Claude home:
python3 <installed-skill>/scripts/install.py --claude-home PATH
The installer owns three files under the selected Claude home's hooks/
directory and merges the skill's registrations into settings.json. It preserves
unrelated settings and hook registrations. Malformed settings, conflicting
registrations, symlinks, and unowned managed-path files are no-write failures.
Its scope is the bundled files and registrations.
To merge the registrations into a settings file kept outside the Claude home, for example one versioned in a dotfiles checkout:
python3 <installed-skill>/scripts/install.py --claude-home PATH \
--settings-file PATH/TO/settings.json
--settings-file moves only the settings file. The three hook files, and the
paths rendered into the registrations, still follow --claude-home. The named
target must be a regular non-symlink file, and its parent directory must already
exist, be a directory, and be writable and searchable; a symlinked target or an
unusable parent is a no-write failure. A parent reached through a symlinked
directory is fine, which is how a versioned checkout is usually wired.
For a consumer that manages its own settings.json hook registrations, add
--skip-settings:
python3 <installed-skill>/scripts/install.py --claude-home PATH --skip-settings
This installs the three managed hook files and stops there: it does not read,
parse, validate, merge, or write settings.json at all. The consumer owns
registering the installed hooks in its own settings. All the other guards
still run: source ownership, the hooks-directory symlink check, and the
unowned-managed-file check. --skip-settings and --settings-file name two
different settings targets, so the installer refuses both together at the
command-line level rather than picking one.
The external tool codebase-memory-mcp installs its own hooks at two of the
same names (cbm-code-discovery-gate, cbm-session-reminder). Activation does
not reclaim a name another tool already owns; it stops and names the likely
owner and the repair (move the foreign files out of hooks/, remove that
tool's registrations from settings.json, then rerun).
Graph-query vocabulary
list_projectsandindex_statusreport indexed-project inventory and health.get_architecturesummarizes repository structure and architectural relationships.search_graphlocates symbols by name, label, or qualified-name pattern.trace_pathfollows callers, callees, data flow, and cross-service paths.get_code_snippetreturns exact source for a resolved symbol.query_graphhandles complex multi-hop graph questions.search_codesearches literal source text.detect_changesmaps a Git diff to its structural impact.index_repositorycreates or refreshes a repository graph.
Files (skills)
-
agents
-
openai.yaml 209 B
interface: display_name: "Codebase Memory" short_description: "Explore indexed code through its knowledge graph" default_prompt: "Use $codebase-memory to explore this repository through the code graph."
-
-
config
-
claude-settings.json 1.1 KB
{ "hooks": { "PreToolUse": [ { "matcher": "Grep|Glob", "hooks": [ { "type": "command", "command": "<CLAUDE_HOME>/hooks/cbm-code-discovery-gate", "timeout": 5 } ] } ], "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "<CLAUDE_HOME>/hooks/cbm-session-reminder" } ] }, { "matcher": "resume", "hooks": [ { "type": "command", "command": "<CLAUDE_HOME>/hooks/cbm-session-reminder" } ] }, { "matcher": "clear", "hooks": [ { "type": "command", "command": "<CLAUDE_HOME>/hooks/cbm-session-reminder" } ] }, { "matcher": "compact", "hooks": [ { "type": "command", "command": "<CLAUDE_HOME>/hooks/cbm-session-reminder" } ] } ] } }
-
-
hooks
-
cbm-code-discovery-gate 523 B · in bundle
-
cbm-session-reminder 196 B · in bundle
-
-
scripts
-
install.py 10.6 KB
#!/usr/bin/env python3 """Activate the codebase-memory skill's Claude Code discovery hooks.""" from __future__ import annotations import argparse import json import os import shlex import stat import tempfile from pathlib import Path SKILL_DIRECTORY = Path(__file__).resolve().parents[1] OWNERSHIP = "Managed by codebase-memory skill installer." MANAGED_FILES = { "cbm-code-discovery-gate": (SKILL_DIRECTORY / "hooks" / "cbm-code-discovery-gate", 0o755), "cbm-session-reminder": (SKILL_DIRECTORY / "hooks" / "cbm-session-reminder", 0o755), "cbm-code-discovery-reminder.md": (SKILL_DIRECTORY / "reminder.md", 0o644), } def lstat_or_none(path: Path) -> os.stat_result | None: try: return path.lstat() except FileNotFoundError: return None def arguments() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Activate codebase-memory discovery hooks in Claude Code." ) parser.add_argument( "--claude-home", type=Path, default=Path.home() / ".claude", help="Claude Code home directory (default: $HOME/.claude)", ) settings_target = parser.add_mutually_exclusive_group() settings_target.add_argument( "--settings-file", type=Path, default=None, help=( "settings file to merge registrations into " "(default: <claude-home>/settings.json)" ), ) settings_target.add_argument( "--skip-settings", action="store_true", help=( "Install the managed hook files only; do not read, parse, validate, " "merge, or write settings.json. For a consumer that owns its own " "hook registrations. Not allowed together with --settings-file: a " "named settings target is meaningless when no settings file is " "touched at all." ), ) return parser.parse_args() def atomic_write(path: Path, content: bytes, mode: int) -> None: if path.exists() and path.read_bytes() == content and stat.S_IMODE(path.stat().st_mode) == mode: return descriptor, temporary_name = tempfile.mkstemp( prefix=".codebase-memory-install-", dir=path.parent ) temporary = Path(temporary_name) try: with os.fdopen(descriptor, "wb") as handle: handle.write(content) handle.flush() os.fsync(handle.fileno()) os.chmod(temporary, mode) os.replace(temporary, path) finally: try: temporary.unlink() except FileNotFoundError: pass def command_target(command: object) -> str | None: if not isinstance(command, str): return None try: words = shlex.split(command) except ValueError: return None if not words: return None return os.path.abspath(os.path.expandvars(os.path.expanduser(words[0]))) def unresolved_managed_reference(command: object) -> str | None: """Detect a registration whose target still contains an unresolved variable (an unset one, since $HOME/${HOME} now resolve above) but whose final path component names a managed file. Such a registration cannot be matched to a resolved target, so it must fail closed as a conflict rather than silently pass the merge as an unrelated command. """ if not isinstance(command, str): return None try: words = shlex.split(command) except ValueError: return None if not words: return None expanded = os.path.expandvars(os.path.expanduser(words[0])) if "$" not in expanded: return None if os.path.basename(expanded) not in MANAGED_FILES: return None return command def merge_settings(existing: dict, canonical: dict) -> dict: if not isinstance(existing, dict): raise ValueError("settings root must be an object") hooks = existing.setdefault("hooks", {}) if not isinstance(hooks, dict): raise ValueError("settings hooks must be an object") expected_by_target: dict[str, list[tuple[str, dict]]] = {} for event, entries in canonical["hooks"].items(): for entry in entries: for hook in entry["hooks"]: target = command_target(hook["command"]) if target is None: raise ValueError("canonical managed hook command is not executable") expected_by_target.setdefault(target, []).append((event, entry)) for event, entries in hooks.items(): if not isinstance(entries, list): raise ValueError(f"settings hooks.{event} must be an array") for entry in entries: if not isinstance(entry, dict): raise ValueError(f"settings hooks.{event} entries must be objects") entry_hooks = entry.get("hooks", []) if not isinstance(entry_hooks, list): raise ValueError(f"settings hooks.{event} entry hooks must be an array") for hook in entry_hooks: if not isinstance(hook, dict): continue unresolved = unresolved_managed_reference(hook.get("command")) if unresolved is not None: raise ValueError( f"conflicting managed hook registration for {unresolved}" ) managed_targets = { command_target(hook.get("command")) for hook in entry_hooks if isinstance(hook, dict) and command_target(hook.get("command")) in expected_by_target } for target in managed_targets: if not any( event == expected_event and entry == expected_entry for expected_event, expected_entry in expected_by_target[target] ): raise ValueError( f"conflicting managed hook registration for {target}" ) for event, required_entries in canonical["hooks"].items(): current_entries = hooks.setdefault(event, []) for required in required_entries: matches = [index for index, entry in enumerate(current_entries) if entry == required] if not matches: current_entries.append(required) continue for index in reversed(matches[1:]): del current_entries[index] return existing def main() -> int: options = arguments() claude_home = options.claude_home.expanduser().absolute() skip_settings = options.skip_settings hooks_directory = claude_home / "hooks" settings_path = None settings_label = None settings_stat = None if not skip_settings: if options.settings_file is None: settings_path = claude_home / "settings.json" settings_label = "settings.json" else: settings_path = options.settings_file.expanduser().absolute() settings_label = str(settings_path) container = settings_path.parent if not container.is_dir() or not os.access(container, os.W_OK | os.X_OK): raise SystemExit( f"settings file needs an existing writable directory: {container}" ) settings_stat = lstat_or_none(settings_path) if settings_stat is not None and not stat.S_ISREG(settings_stat.st_mode): raise SystemExit(f"{settings_label} must be a regular non-symlink file") hooks_stat = lstat_or_none(hooks_directory) if hooks_stat is not None and not stat.S_ISDIR(hooks_stat.st_mode): raise SystemExit("hooks must be a real non-symlink directory") planned_files = {} for name, (source, mode) in MANAGED_FILES.items(): content = source.read_bytes() if OWNERSHIP.encode() not in content: raise SystemExit(f"source is missing ownership text: {source}") planned_files[name] = (content, mode) if hooks_stat is not None: for name in MANAGED_FILES: destination = hooks_directory / name destination_stat = lstat_or_none(destination) if destination_stat is None: continue if not stat.S_ISREG(destination_stat.st_mode): raise SystemExit( f"managed target must be a regular non-symlink file: {destination}" ) if OWNERSHIP.encode() not in destination.read_bytes(): raise SystemExit( f"managed target is not owned: {destination} " "(codebase-memory-mcp is known to install its own hooks at " "this name; move its files out of the hooks directory, " "remove its registrations from settings.json, then rerun)" ) if skip_settings: claude_home.mkdir(parents=True, exist_ok=True) hooks_directory.mkdir(exist_ok=True) for name, (planned_content, mode) in planned_files.items(): atomic_write(hooks_directory / name, planned_content, mode) print( f"Installed codebase-memory discovery hook files in {claude_home}; " "no settings.json registrations were written." ) return 0 template = json.loads( (SKILL_DIRECTORY / "config" / "claude-settings.json").read_text( encoding="utf-8" ) ) quoted_home = shlex.quote(str(claude_home)) for entries in template["hooks"].values(): for entry in entries: for hook in entry["hooks"]: hook["command"] = hook["command"].replace( "<CLAUDE_HOME>", quoted_home ) canonical = template if settings_stat is not None: try: settings = json.loads(settings_path.read_text(encoding="utf-8")) except json.JSONDecodeError as error: raise SystemExit( f"{settings_label} is not valid JSON: {error.msg}" ) from error settings_mode = stat.S_IMODE(settings_stat.st_mode) else: settings = {} settings_mode = 0o600 try: merged = merge_settings(settings, canonical) except ValueError as error: raise SystemExit(str(error)) from error content = (json.dumps(merged, indent=2) + "\n").encode() claude_home.mkdir(parents=True, exist_ok=True) hooks_directory.mkdir(exist_ok=True) for name, (planned_content, mode) in planned_files.items(): atomic_write(hooks_directory / name, planned_content, mode) atomic_write(settings_path, content, settings_mode) print(f"Activated codebase-memory discovery hooks in {claude_home}") return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
reminder.md 1.7 KB
Managed by codebase-memory skill installer. # Code discovery policy Use codebase-memory-mcp graph tools first for structural code exploration, against exactly one project: the one that belongs to the checkout you are working in. Establish that project before querying. When a workflow supplies a `project` for the checkout it verified, use exactly that name as given. Otherwise resolve the canonical current checkout through the supported structured interface, `python3 <cbm-onboard-skill-directory>/scripts/cbm-lifecycle.py ensure <checkout path>`, and use the `project` it prints. Never pick the graph by project name, branch-like label, list order, apparent recency, or because it was the only result; `list_projects` is an inventory, not a way to choose the current checkout. A reported `unavailable`, or no usable interface at all, means ordinary search and file reads for the rest of the session. Follow cbm-onboard's bounded diagnostic and retry sequence: distinguish a missing or unsupported binary from an unable-to-respond CLI; retry the same `ensure` command with its documented local-only sandbox rationale only after the normal workspace-write attempt. An active-generation conflict means wait and retry, never a sandbox denial or permission to close another session. With that project established, use `search_graph` to find symbols, `trace_path` for callers and callees, `get_code_snippet` for exact source, `query_graph` for multi-hop questions, and `get_architecture` for orientation. Use `search_code` or ordinary search and file reads for literal text, configuration, non-code files, and unindexed projects. Activating this skill never indexes a project. Run `index_repository` only when indexing is explicitly requested or required by the target repository. -
SKILL.md 3.6 KB
--- name: codebase-memory description: Use the codebase knowledge graph for structural code queries, trace call paths and dependencies, inspect architecture, assess change impact, or activate the bundled Claude Code discovery hooks. --- # Codebase Memory Follow [reminder.md](reminder.md) for the standing discovery policy; it is the one authored policy shared by this skill, the profile pointer, and the installed SessionStart hook. When `ensure` reports unavailable, use its cbm-onboard diagnostic and bounded retry sequence rather than guessing at a project or treating every failure as a sandbox denial. The reminder owns the details. ## Activate discovery guidance in Claude Code After the standard skills installer copies this directory, run: ```sh python3 ~/.claude/skills/codebase-memory/scripts/install.py ``` For a non-default Claude home: ```sh python3 <installed-skill>/scripts/install.py --claude-home PATH ``` The installer owns three files under the selected Claude home's `hooks/` directory and merges the skill's registrations into `settings.json`. It preserves unrelated settings and hook registrations. Malformed settings, conflicting registrations, symlinks, and unowned managed-path files are no-write failures. Its scope is the bundled files and registrations. To merge the registrations into a settings file kept outside the Claude home, for example one versioned in a dotfiles checkout: ```sh python3 <installed-skill>/scripts/install.py --claude-home PATH \ --settings-file PATH/TO/settings.json ``` `--settings-file` moves only the settings file. The three hook files, and the paths rendered into the registrations, still follow `--claude-home`. The named target must be a regular non-symlink file, and its parent directory must already exist, be a directory, and be writable and searchable; a symlinked target or an unusable parent is a no-write failure. A parent reached through a symlinked directory is fine, which is how a versioned checkout is usually wired. For a consumer that manages its own `settings.json` hook registrations, add `--skip-settings`: ```sh python3 <installed-skill>/scripts/install.py --claude-home PATH --skip-settings ``` This installs the three managed hook files and stops there: it does not read, parse, validate, merge, or write `settings.json` at all. The consumer owns registering the installed hooks in its own settings. All the other guards still run: source ownership, the hooks-directory symlink check, and the unowned-managed-file check. `--skip-settings` and `--settings-file` name two different settings targets, so the installer refuses both together at the command-line level rather than picking one. The external tool `codebase-memory-mcp` installs its own hooks at two of the same names (`cbm-code-discovery-gate`, `cbm-session-reminder`). Activation does not reclaim a name another tool already owns; it stops and names the likely owner and the repair (move the foreign files out of `hooks/`, remove that tool's registrations from `settings.json`, then rerun). ## Graph-query vocabulary - `list_projects` and `index_status` report indexed-project inventory and health. - `get_architecture` summarizes repository structure and architectural relationships. - `search_graph` locates symbols by name, label, or qualified-name pattern. - `trace_path` follows callers, callees, data flow, and cross-service paths. - `get_code_snippet` returns exact source for a resolved symbol. - `query_graph` handles complex multi-hop graph questions. - `search_code` searches literal source text. - `detect_changes` maps a Git diff to its structural impact. - `index_repository` creates or refreshes a repository graph.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.