From 14eb62ce7aabbd39963017bc4c0fd4b3df75efdc Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 25 Sep 2026 07:52:05 +0300 Subject: [PATCH 1/2] feat(hooks): make Claude Code search through the index by default Agents with the skill installed still reached for Grep out of habit. Add `codebase-index install-hooks [--global]`, which merges two hooks into the Claude Code settings while keeping every other hook: - SessionStart: in an indexed repository, a one-line note naming the commands that answer code questions. - PreToolUse on Grep|Bash: the first code search of a session, before any codebase-index command has run, is sent back with the index command to use instead. Repeating the call lets it through, the first index command turns the guard off for the session, and searches over docs, logs or config, `ps | grep` pipelines and unindexed directories pass untouched. The hooks run as `codebase-index-hook`, a stdlib-only entry point (about 0.1 s per call); CBX_GUARD=0 disables the guard. The plugin registers the same hooks. --- CHANGELOG.md | 16 +++ README.md | 5 +- hooks/hooks.json | 17 +++ pyproject.toml | 1 + src/codebase_index/cli.py | 35 +++++ src/codebase_index/hooks.py | 256 +++++++++++++++++++++++++++++++++ src/codebase_index/scaffold.py | 61 ++++++++ tests/test_hooks_guard.py | 120 ++++++++++++++++ 8 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 src/codebase_index/hooks.py create mode 100644 tests/test_hooks_guard.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 442ed6c..6d12b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +### Added + +- `codebase-index install-hooks [--global]` makes Claude Code search through the index + by default. It merges two hooks into `.claude/settings.json` (or + `~/.claude/settings.json`), keeping every other hook: + - `SessionStart`: in an indexed repository, one line of context naming the commands + that answer code questions; + - `PreToolUse` on `Grep|Bash`: the first code search of a session, before any + `codebase-index` command has run, is sent back with the index command to use + instead. Repeating the same call lets it through, the first index command turns the + guard off for the session, and searches over docs, logs or config, pipelines such + as `ps | grep`, and directories without an index are never intercepted. + `CBX_GUARD=0` disables it; `install-hooks --uninstall` removes both hooks. + The hooks run as `codebase-index-hook`, a standard-library-only entry point (about + 0.1 s per call). The Claude Code plugin registers the same hooks. + ## [2.1.0] - 2026-09-24 Agent-efficiency release. An agent answering five code questions on a 5.9k-file diff --git a/README.md b/README.md index ba3aa4d..6ad5799 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,10 @@ agent format: `path:start-end symbol` with the matching lines numbered underneat | **Any MCP client** (Claude Desktop, Cursor, VS Code, Zed, Windsurf, ...) | `pip install "codebase-index[mcp]"` then `codebase-index mcp --root /path/to/repo` | 11 tools (`search_code`, `find_refs`, `impact_of`, `impact_of_diff`, `path_between`, ...) with a versioned JSON envelope | | **Anything with a shell** | `codebase-index --json ...` | The same payloads as plain JSON, plus a local SQLite database you can query yourself | -`init --target auto` detects which of these are present. See +`init --target auto` detects which of these are present. To make Claude Code reach for +the index before Grep in every indexed repository, run +`codebase-index install-hooks --global`: a session-start note plus a guard that sends the +first code search of a session to the index (repeat the call to run it anyway). See [INSTALLATION.md](docs/INSTALLATION.md) and [MCP.md](docs/MCP.md). ## Evidence diff --git a/hooks/hooks.json b/hooks/hooks.json index 886df9d..bd59d21 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -6,6 +6,23 @@ { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.sh\"" + }, + { + "type": "command", + "command": "codebase-index-hook session-start 2>/dev/null || true", + "timeout": 5 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Grep|Bash", + "hooks": [ + { + "type": "command", + "command": "codebase-index-hook guard 2>/dev/null || true", + "timeout": 5 } ] } diff --git a/pyproject.toml b/pyproject.toml index 4761765..2e61137 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ build = ["build>=1.2", "twine>=5.0"] codebase-index = "codebase_index.cli:app" cbx = "codebase_index.cli:app" # short alias codebase-index-mcp = "codebase_index.mcp.server:run" # standalone MCP entry point +codebase-index-hook = "codebase_index.hooks:main" # Claude Code hooks (stdlib only, fast) [project.urls] Homepage = "https://github.com/denfry/codebase-index" diff --git a/src/codebase_index/cli.py b/src/codebase_index/cli.py index 9d61b2c..85170f4 100644 --- a/src/codebase_index/cli.py +++ b/src/codebase_index/cli.py @@ -1064,6 +1064,41 @@ def watch( raise typer.Exit(code=1) +@app.command("install-hooks") +def install_hooks( + ctx: typer.Context, + global_: bool = typer.Option( + False, "--global", help="Install into ~/.claude/settings.json (every project)." + ), + uninstall: bool = typer.Option(False, "--uninstall", help="Remove the hooks instead."), +) -> None: + """Make Claude Code search through the index by default. + + Adds a SessionStart note and a PreToolUse guard on Grep/Bash: the first code + search of a session, before any codebase-index command, is sent back with the + index command to run instead; repeating it lets it through. Both do nothing in + a directory without an index. Other hooks in the settings file are kept. + """ + from .config import find_root + from . import scaffold + + if global_: + path = Path.home() / ".claude" / "settings.json" + else: + root_opt = ctx.obj.get("root") if ctx.obj else None + path = (Path(root_opt).resolve() if root_opt else find_root()) / scaffold.SETTINGS_REL + if uninstall: + changed = scaffold.uninstall_guard_hooks(path) + verb = "removed from" + else: + changed = scaffold.install_guard_hooks(path) + verb = "added to" + if changed: + typer.echo(f"[install-hooks] {', '.join(changed)} {verb} {path}") + else: + typer.echo(f"[install-hooks] nothing to change in {path}") + + @app.command("skill-update") def skill_update( ctx: typer.Context, diff --git a/src/codebase_index/hooks.py b/src/codebase_index/hooks.py new file mode 100644 index 0000000..a57dfb2 --- /dev/null +++ b/src/codebase_index/hooks.py @@ -0,0 +1,256 @@ +"""Claude Code hooks that make the index the default way to search code. + +Two events, both no-ops outside a repository that has an index: + +- ``session-start`` adds one short note to the session context: this repository + is indexed, and which commands answer code questions. +- ``guard`` (PreToolUse on Grep and Bash) intercepts a code search the first time + in a session, before any ``codebase-index`` command has run, and names the + index command to use instead. It is a nudge, not a wall: repeating the same + call lets it through, the first ``codebase-index`` command in the session turns + the guard off, and searches over docs, logs or config are never intercepted. + +Measured on a 5.9k-file monorepo, an agent spent 19% fewer tokens with the index +than with grep (tests/eval/results/2026-09-24-agent-pilot.md), but agents reach +for Grep out of habit. The guard runs on every Bash call, so this module imports +only the standard library and does no index work beyond one small SQLite read. + +Entry point: ``codebase-index-hook `` reads the hook JSON on stdin and +prints the hook JSON reply (or nothing). ``CBX_GUARD=0`` disables the guard. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import sqlite3 +import sys +import time +from pathlib import Path +from typing import Any, Optional + +INDEX_REL = Path(".claude") / "cache" / "codebase-index" / "index.sqlite" +_STATE_DIR = "hook-sessions" +_STATE_TTL_S = 2 * 24 * 3600 + +# A shell segment that searches file contents: the first command of the segment +# (after `&&`, `||`, `;` or the start), so `ps aux | grep x` is not a repo search. +_SEARCH_CMD = re.compile( + r"(?:^|&&|\|\||;)\s*(?:cd\s+\S+\s*&&\s*)?" + r"(?Pgit\s+grep|grep|egrep|fgrep|rg|ag|ack|findstr|Select-String)\b(?P[^|;&]*)", + re.IGNORECASE, +) +_INDEX_CMD = re.compile(r"(?:^|[\s;&|/\\\"'])(?:codebase-index|cbx)(?:\.exe|\.ps1)?\s+\w") +# Searches over prose, logs and config are what grep is for; leave them alone. +_NON_CODE = re.compile( + r"\.(?:md|mdx|rst|txt|log|ya?ml|json|toml|ini|cfg|conf|csv|xml|properties|lock|env|html?)\b" + r"|--include=\*?\.(?:md|txt|log|ya?ml|json|toml)", + re.IGNORECASE, +) +_NON_CODE_TYPES = {"md", "markdown", "txt", "log", "yaml", "yml", "json", "toml", "xml", + "csv", "ini", "config"} + + +def main(argv: Optional[list[str]] = None) -> int: + args = sys.argv[1:] if argv is None else argv + event = args[0] if args else "" + try: + payload = json.loads(sys.stdin.read() or "{}") + except (ValueError, OSError): + return 0 + try: + reply = {"guard": guard, "session-start": session_start}.get(event, _none)(payload) + except Exception: # a hook must never break the session it runs in + return 0 + if reply: + sys.stdout.write(json.dumps(reply)) + return 0 + + +def _none(_payload: dict) -> Optional[dict]: + return None + + +def find_index_root(start: Path) -> Optional[Path]: + """The nearest directory at or above `start` that holds an index.""" + for path in (start, *start.parents): + if (path / INDEX_REL).is_file(): + return path + return None + + +def _cwd(payload: dict) -> Path: + raw = str(payload.get("cwd") or os.getcwd()) + # Git Bash hands out MSYS paths (/c/Projects/x); Python on Windows needs C:/. + msys = re.match(r"^/([a-zA-Z])(/.*)?$", raw) + if os.name == "nt" and msys: + raw = f"{msys.group(1)}:{msys.group(2) or '/'}" + return Path(raw) + + +# --- session-start ------------------------------------------------------------------ + + +def session_start(payload: dict) -> Optional[dict]: + root = find_index_root(_cwd(payload)) + if root is None: + return None + files = _indexed_files(root) + size = f" ({files} files)" if files else "" + note = ( + f"This repository has a codebase-index index{size}. For questions about the " + "code (where something is, how it works, who calls it, what a change breaks) " + "use the codebase-index skill before Grep or Read: " + '`codebase-index search "" --compact`, ' + '`codebase-index refs "Owner.member" --compact`, ' + '`codebase-index impact "Owner.member" --compact`. ' + "Output lines are numbered; cite them as file:line." + ) + return {"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": note}} + + +def _indexed_files(root: Path) -> Optional[int]: + try: + conn = sqlite3.connect(f"file:{root / INDEX_REL}?mode=ro", uri=True, timeout=0.5) + try: + return int(conn.execute("SELECT COUNT(*) FROM files").fetchone()[0]) + finally: + conn.close() + except sqlite3.Error: + return None + + +# --- guard ------------------------------------------------------------------------------ + + +def guard(payload: dict) -> Optional[dict]: + if os.environ.get("CBX_GUARD", "").strip() == "0": + return None + tool = payload.get("tool_name") or "" + tool_input = payload.get("tool_input") or {} + if tool not in ("Grep", "Bash"): + return None + root = find_index_root(_cwd(payload)) + if root is None: + return None + session = str(payload.get("session_id") or "default") + state = _State(root, session) + + if tool == "Bash": + command = str(tool_input.get("command") or "") + if _INDEX_CMD.search(command): + state.mark_used() + return None + pattern = _shell_search_pattern(command) + if pattern is None: + return None + else: + if _grep_targets_non_code(tool_input): + return None + pattern = str(tool_input.get("pattern") or "") + + if state.used: + return None + fingerprint = hashlib.sha1( + (tool + json.dumps(tool_input, sort_keys=True, default=str)).encode("utf-8") + ).hexdigest() + if state.was_denied(fingerprint): + return None # the retry after a nudge: the agent decided grep is right + state.deny(fingerprint) + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": _reason(pattern), + } + } + + +def _shell_search_pattern(command: str) -> Optional[str]: + """The search term of a content search in `command`, or None if there is none.""" + for match in _SEARCH_CMD.finditer(command): + args = match.group("args") + if _NON_CODE.search(args): + continue + quoted = re.search(r"""(['"])(.+?)\1""", args) + if quoted: + return quoted.group(2) + words = [w for w in args.split() if not w.startswith("-")] + return words[0] if words else "" + return None + + +def _grep_targets_non_code(tool_input: dict) -> bool: + kind = str(tool_input.get("type") or "").lower() + if kind in _NON_CODE_TYPES: + return True + target = f"{tool_input.get('glob') or ''} {tool_input.get('path') or ''}" + return bool(_NON_CODE.search(target)) + + +def _reason(pattern: str) -> str: + term = re.sub(r"[\\^$()\[\]{}|*+?]", " ", pattern).strip() or "" + term = " ".join(term.split())[:80] + return ( + "codebase-index: this repository is indexed, and the index answers code " + "searches with ranked, numbered lines in fewer tokens than grep. Run it first:\n" + f' codebase-index search "{term}" --compact # where / how\n' + ' codebase-index refs "Owner.member" --compact # every call site, with callers\n' + ' codebase-index symbol "Name" # a definition\n' + "If the index does not answer, repeat this exact call and it will run. Once any " + "codebase-index command has run in this session, searches are not intercepted." + ) + + +class _State: + """Per-session guard state: whether the index was used, which calls were nudged.""" + + def __init__(self, root: Path, session: str) -> None: + safe = re.sub(r"[^A-Za-z0-9_.-]", "_", session)[:80] or "default" + self.dir = root / INDEX_REL.parent / _STATE_DIR + self.path = self.dir / f"{safe}.json" + self.data: dict[str, Any] = {"used": False, "denied": []} + try: + self.data.update(json.loads(self.path.read_text(encoding="utf-8"))) + except (OSError, ValueError): + pass + + @property + def used(self) -> bool: + return bool(self.data.get("used")) + + def was_denied(self, fingerprint: str) -> bool: + return fingerprint in self.data.get("denied", []) + + def mark_used(self) -> None: + if not self.used: + self.data["used"] = True + self._save() + + def deny(self, fingerprint: str) -> None: + self.data.setdefault("denied", []).append(fingerprint) + self._save() + + def _save(self) -> None: + try: + self.dir.mkdir(parents=True, exist_ok=True) + self.path.write_text(json.dumps(self.data), encoding="utf-8") + _prune(self.dir) + except OSError: + pass + + +def _prune(directory: Path) -> None: + cutoff = time.time() - _STATE_TTL_S + for item in directory.glob("*.json"): + try: + if item.stat().st_mtime < cutoff: + item.unlink() + except OSError: + pass + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/codebase_index/scaffold.py b/src/codebase_index/scaffold.py index 72a65c4..4862f14 100644 --- a/src/codebase_index/scaffold.py +++ b/src/codebase_index/scaffold.py @@ -283,6 +283,67 @@ def merge_hook_settings(root: Path) -> bool: return True +# Index-first hooks (hooks.py): a SessionStart note and a PreToolUse guard that +# steers the first code search of a session to the index. +_GUARD_MARKER = "codebase-index-hook" +GUARD_HOOKS: dict[str, dict] = { + "SessionStart": { + "hooks": [{"type": "command", "command": "codebase-index-hook session-start", + "timeout": 5}], + }, + "PreToolUse": { + "matcher": "Grep|Bash", + "hooks": [{"type": "command", "command": "codebase-index-hook guard", "timeout": 5}], + }, +} + + +def _is_guard_entry(entry: dict) -> bool: + return any(_GUARD_MARKER in str(h.get("command", "")) for h in entry.get("hooks", [])) + + +def install_guard_hooks(settings_path: Path) -> list[str]: + """Merge the index-first hooks into a Claude Code settings file. + + Idempotent, and leaves every other hook in place. Returns the events added. + """ + settings: dict = {} + if settings_path.exists(): + settings = json.loads(settings_path.read_text(encoding="utf-8") or "{}") + hooks = settings.setdefault("hooks", {}) + added = [] + for event, entry in GUARD_HOOKS.items(): + entries = hooks.setdefault(event, []) + if not any(_is_guard_entry(e) for e in entries): + entries.append(json.loads(json.dumps(entry))) + added.append(event) + if added: + settings_path.parent.mkdir(parents=True, exist_ok=True) + settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8") + return added + + +def uninstall_guard_hooks(settings_path: Path) -> list[str]: + """Remove the index-first hooks; other hooks are kept. Returns the events changed.""" + if not settings_path.exists(): + return [] + settings = json.loads(settings_path.read_text(encoding="utf-8") or "{}") + hooks = settings.get("hooks", {}) + removed = [] + for event in GUARD_HOOKS: + entries = hooks.get(event, []) + kept = [e for e in entries if not _is_guard_entry(e)] + if len(kept) != len(entries): + removed.append(event) + if kept: + hooks[event] = kept + else: + hooks.pop(event, None) + if removed: + settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8") + return removed + + # ── MCP client config helpers ────────────────────────────────────────────────────────────────── _MCP_SERVER_NAME = "codebase-index" diff --git a/tests/test_hooks_guard.py b/tests/test_hooks_guard.py new file mode 100644 index 0000000..86365a8 --- /dev/null +++ b/tests/test_hooks_guard.py @@ -0,0 +1,120 @@ +"""Index-first hooks: a SessionStart note and a PreToolUse guard (hooks.py).""" + +from __future__ import annotations + +import io +import json +from pathlib import Path + +import pytest + +from codebase_index import hooks, scaffold + + +@pytest.fixture +def repo(tmp_path: Path) -> Path: + db = tmp_path / "repo" / hooks.INDEX_REL + db.parent.mkdir(parents=True) + import sqlite3 + + conn = sqlite3.connect(db) + conn.execute("CREATE TABLE files (id INTEGER PRIMARY KEY)") + conn.executemany("INSERT INTO files VALUES (?)", [(i,) for i in range(3)]) + conn.commit() + conn.close() + return tmp_path / "repo" + + +def _call(root: Path, tool: str, session: str = "s1", **tool_input): + return hooks.guard({"session_id": session, "cwd": str(root / "src"), + "tool_name": tool, "tool_input": tool_input}) + + +def _denied(reply) -> bool: + return bool(reply) and reply["hookSpecificOutput"]["permissionDecision"] == "deny" + + +def test_session_start_notes_the_index_only_where_one_exists(repo: Path, tmp_path: Path): + reply = hooks.session_start({"cwd": str(repo)}) + context = reply["hookSpecificOutput"]["additionalContext"] + assert "(3 files)" in context and "--compact" in context + assert hooks.session_start({"cwd": str(tmp_path)}) is None + + +def test_first_code_search_is_sent_to_the_index_and_its_retry_runs(repo: Path): + first = _call(repo, "Grep", pattern="TownService.refresh") + assert _denied(first) + reason = first["hookSpecificOutput"]["permissionDecisionReason"] + assert 'codebase-index search "TownService.refresh" --compact' in reason + assert _call(repo, "Grep", pattern="TownService.refresh") is None # the retry + assert _denied(_call(repo, "Grep", pattern="somethingElse")) # a new search + + +def test_using_the_index_turns_the_guard_off_for_the_session(repo: Path): + assert _call(repo, "Bash", command="cd x && codebase-index refs A.b --compact") is None + assert _call(repo, "Grep", pattern="anything") is None + assert _call(repo, "Bash", command="rg -n foo src") is None + assert _denied(_call(repo, "Grep", session="other", pattern="anything")) + + +@pytest.mark.parametrize("command", [ + "grep -rn TODO docs/*.md", + "ps aux | grep java", + "git log --oneline | grep fix", + "ls -la", + "rg -n error build.log", +]) +def test_non_code_and_non_search_shell_commands_pass(repo: Path, command: str): + assert _call(repo, "Bash", command=command) is None + + +@pytest.mark.parametrize("command", [ + "grep -rn treasurerDeparted --include=*.java .", + "cd /c/Projects/x && rg -n 'TownService.refresh' realism-polity", + "git grep -n place", +]) +def test_code_searches_in_the_shell_are_guarded(repo: Path, command: str): + assert _denied(_call(repo, "Bash", command=command)) + + +def test_grep_over_docs_or_config_passes(repo: Path): + assert _call(repo, "Grep", pattern="x", glob="**/*.yml") is None + assert _call(repo, "Grep", pattern="x", type="md") is None + + +def test_no_index_or_disabled_guard_never_intercepts(repo: Path, tmp_path: Path, monkeypatch): + assert _call(tmp_path, "Grep", pattern="x") is None + monkeypatch.setenv("CBX_GUARD", "0") + assert _call(repo, "Grep", pattern="x") is None + + +def test_msys_cwd_is_understood(repo: Path, monkeypatch): + monkeypatch.setattr(hooks.os, "name", "nt") + drive, rest = str(repo).replace("\\", "/").split(":", 1) if ":" in str(repo) else ("", "") + if not drive: + pytest.skip("POSIX path has no drive letter") + assert hooks._cwd({"cwd": f"/{drive.lower()}{rest}"}) == Path(f"{drive}:{rest}") + + +def test_main_reads_stdin_and_writes_the_reply(repo: Path, monkeypatch, capsys): + payload = {"session_id": "m", "cwd": str(repo), "tool_name": "Grep", + "tool_input": {"pattern": "x"}} + monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload))) + assert hooks.main(["guard"]) == 0 + assert json.loads(capsys.readouterr().out)["hookSpecificOutput"]["permissionDecision"] == "deny" + monkeypatch.setattr("sys.stdin", io.StringIO("not json")) + assert hooks.main(["guard"]) == 0 and capsys.readouterr().out == "" + + +def test_install_merges_idempotently_and_uninstall_keeps_other_hooks(tmp_path: Path): + path = tmp_path / "settings.json" + other = {"type": "command", "command": "my-other-hook"} + path.write_text(json.dumps({"model": "x", "hooks": { + "PreToolUse": [{"matcher": "Bash", "hooks": [other]}]}}), encoding="utf-8") + assert scaffold.install_guard_hooks(path) == ["SessionStart", "PreToolUse"] + assert scaffold.install_guard_hooks(path) == [] + data = json.loads(path.read_text(encoding="utf-8")) + assert data["model"] == "x" and len(data["hooks"]["PreToolUse"]) == 2 + assert scaffold.uninstall_guard_hooks(path) == ["SessionStart", "PreToolUse"] + data = json.loads(path.read_text(encoding="utf-8")) + assert data["hooks"] == {"PreToolUse": [{"matcher": "Bash", "hooks": [other]}]} From 2f981fa1e5f1a28b4daa5530388f1c109a7e80a1 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 25 Sep 2026 08:01:37 +0300 Subject: [PATCH 2/2] =?UTF-8?q?release:=20v2.1.1=20=E2=80=94=20index-first?= =?UTF-8?q?=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bump with every mirror synced (plugin.json, requirements.lock, skill stamps) and the 2.1.1 release notes. --- .claude-plugin/plugin.json | 2 +- .claude/skills/codebase-index/.skill_version | 2 +- .codex/skills/codebase-index/.skill_version | 2 +- .opencode/skills/codebase-index/.skill_version | 2 +- CHANGELOG.md | 5 ++++- docs/installer.md | 2 +- requirements.lock | 2 +- src/codebase_index/__init__.py | 2 +- 8 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8f6dd3c..4fe56c7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -3,7 +3,7 @@ "name": "codebase-index", "displayName": "Codebase Index", "description": "Give Claude a precise local map of your codebase: find implementations, trace behavior, and predict change impact with file-line evidence.", - "version": "2.1.0", + "version": "2.1.1", "author": { "name": "codebase-index contributors" }, diff --git a/.claude/skills/codebase-index/.skill_version b/.claude/skills/codebase-index/.skill_version index 7ec1d6d..3e3c2f1 100644 --- a/.claude/skills/codebase-index/.skill_version +++ b/.claude/skills/codebase-index/.skill_version @@ -1 +1 @@ -2.1.0 +2.1.1 diff --git a/.codex/skills/codebase-index/.skill_version b/.codex/skills/codebase-index/.skill_version index 7ec1d6d..3e3c2f1 100644 --- a/.codex/skills/codebase-index/.skill_version +++ b/.codex/skills/codebase-index/.skill_version @@ -1 +1 @@ -2.1.0 +2.1.1 diff --git a/.opencode/skills/codebase-index/.skill_version b/.opencode/skills/codebase-index/.skill_version index 7ec1d6d..3e3c2f1 100644 --- a/.opencode/skills/codebase-index/.skill_version +++ b/.opencode/skills/codebase-index/.skill_version @@ -1 +1 @@ -2.1.0 +2.1.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d12b0c..434af0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +## [2.1.1] - 2026-09-25 + ### Added - `codebase-index install-hooks [--global]` makes Claude Code search through the index @@ -884,7 +886,8 @@ Pooled over 305 queries (Python, Java, TypeScript), v1.8.0 → 1.9.0: - Hooks example + `watch` mode for keeping the index fresh without blocking the edit loop (M8). - `doctor`, `stats`, `clean` diagnostics/maintenance commands. -[Unreleased]: https://github.com/denfry/codebase-index/compare/v2.1.0...HEAD +[Unreleased]: https://github.com/denfry/codebase-index/compare/v2.1.1...HEAD +[2.1.1]: https://github.com/denfry/codebase-index/compare/v2.1.0...v2.1.1 [2.1.0]: https://github.com/denfry/codebase-index/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/denfry/codebase-index/compare/v1.9.0...v2.0.0 [1.10.0]: https://github.com/denfry/codebase-index/compare/v1.9.0...abb67df diff --git a/docs/installer.md b/docs/installer.md index 458c3b5..129ed9b 100644 --- a/docs/installer.md +++ b/docs/installer.md @@ -110,7 +110,7 @@ pwsh ./install.ps1 -Target claude -InstallDir "D:\skills\codebase-index" **Pin to a branch or tag** (reproducibility and safety): ```sh -sh install.sh --branch v2.1.0 +sh install.sh --branch v2.1.1 ``` --- diff --git a/requirements.lock b/requirements.lock index 084a720..d0f2b53 100644 --- a/requirements.lock +++ b/requirements.lock @@ -1,3 +1,3 @@ -codebase-index @ https://github.com/denfry/codebase-index/archive/refs/tags/v2.1.0.tar.gz +codebase-index @ https://github.com/denfry/codebase-index/archive/refs/tags/v2.1.1.tar.gz tree-sitter==0.25.2 tree-sitter-language-pack==1.8.1 diff --git a/src/codebase_index/__init__.py b/src/codebase_index/__init__.py index 40ce013..593777e 100644 --- a/src/codebase_index/__init__.py +++ b/src/codebase_index/__init__.py @@ -4,4 +4,4 @@ See docs/ARCHITECTURE.md for the module map. """ -__version__ = "2.1.0" +__version__ = "2.1.1"