#!/usr/bin/env bash set -euo pipefail BASE_URL="${CODEDB_URL:-https://codedb.codegraff.com}" INSTALL_DIR="${CODEDB_DIR:-$HOME/bin}" # Colors R='\033[0;31m' G='\033[0;32m' Y='\033[0;33m' B='\033[0;34m' C='\033[0;36m' W='\033[1;37m' D='\033[0;90m' N='\033[0m' fetch_latest_version() { local version="" version="$(curl -fsSL -A 'codedb-installer' \ "https://api.github.com/repos/justrach/codedb/releases/latest" 2>/dev/null \ | grep -oE '"tag_name"\s*:\s*"v[^"]*"' \ | cut -d'"' -f4 \ | sed 's/^v//')" || true if [ -z "$version" ]; then version="$(curl -fsSL -A 'codedb-installer' "$BASE_URL/latest.json" 2>/dev/null \ | grep -oE '"version"\s*:\s*"[^"]*"' \ | cut -d'"' -f4)" || true fi printf '%s' "$version" } detect_platform() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$os" in Darwin) os="darwin" ;; Linux) os="linux" ;; MINGW*|MSYS*|CYGWIN*) # #677: we are inside $(...) — printing guidance or exiting here is # swallowed by the subshell. Emit a sentinel for main() instead. echo "windows" return 0 ;; *) printf " ${R}Unsupported OS: $os${N}\n" >&2; exit 1 ;; esac case "$arch" in arm64|aarch64) arch="arm64" ;; x86_64|amd64) arch="x86_64" ;; *) printf " ${R}Unsupported arch: $arch${N}\n" >&2; exit 1 ;; esac echo "${os}-${arch}" } register_claude() { local codedb_bin="$1" local config="$HOME/.claude.json" if ! command -v python3 >/dev/null 2>&1; then printf " ${D}claude: skip (python3 not found)${N}\n" return fi python3 - "$config" "$codedb_bin" << 'PYEOF' import json, sys, os config_path, codedb_bin = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) except (FileNotFoundError, json.JSONDecodeError): data = {} servers = data.setdefault("mcpServers", {}) servers["codedb"] = {"command": codedb_bin, "args": ["mcp"]} with open(config_path, "w") as f: json.dump(data, f, indent=2) f.write("\n") PYEOF printf " ${G}✓${N} claude code ${D}→ $config${N}\n" } register_codex() { local codedb_bin="$1" local config_dir="$HOME/.codex" local config="$config_dir/config.toml" mkdir -p "$config_dir" if [ -f "$config" ] && grep -q '\[mcp_servers\.codedb\]' "$config" 2>/dev/null; then printf " ${G}✓${N} codex ${D}→ $config (already registered)${N}\n" return fi { [ -f "$config" ] && [ -s "$config" ] && echo "" echo '[mcp_servers.codedb]' echo "command = \"$codedb_bin\"" echo 'args = ["mcp"]' echo 'startup_timeout_sec = 30' } >> "$config" printf " ${G}✓${N} codex ${D}→ $config${N}\n" } register_codex_policy() { local codedb_bin="$1" # #680: auto-install the CodeDB-first AGENTS.md policy for Codex sessions. # The binary owns the sticky opt-out (marker + removal receipt), so a user's # removal survives re-runs and the 24h auto-update. Older binaries without # the subcommand skip silently. if "$codedb_bin" codex install >/dev/null 2>&1; then printf " ${G}✓${N} codex policy ${D}→ ~/.codex/AGENTS.md (remove: codedb codex uninstall)${N}\n" fi } register_gemini() { local codedb_bin="$1" local config_dir="$HOME/.gemini" local config="$config_dir/settings.json" if [ ! -d "$config_dir" ]; then return fi if ! command -v python3 >/dev/null 2>&1; then printf " ${D}gemini: skip (python3 not found)${N}\n" return fi python3 - "$config" "$codedb_bin" << 'PYEOF' import json, sys, os config_path, codedb_bin = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) except (FileNotFoundError, json.JSONDecodeError): data = {} servers = data.setdefault("mcpServers", {}) servers["codedb"] = {"command": codedb_bin, "args": ["mcp"]} with open(config_path, "w") as f: json.dump(data, f, indent=2) f.write("\n") PYEOF printf " ${G}✓${N} gemini cli ${D}→ $config${N}\n" } register_cursor() { local codedb_bin="$1" local config_dir="$HOME/.cursor" local config="$config_dir/mcp.json" if [ ! -d "$config_dir" ]; then return fi if ! command -v python3 >/dev/null 2>&1; then printf " ${D}cursor: skip (python3 not found)${N}\n" return fi python3 - "$config" "$codedb_bin" << 'PYEOF' import json, sys, os config_path, codedb_bin = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) except (FileNotFoundError, json.JSONDecodeError): data = {} servers = data.setdefault("mcpServers", {}) servers["codedb"] = {"command": codedb_bin, "args": ["mcp"]} with open(config_path, "w") as f: json.dump(data, f, indent=2) f.write("\n") PYEOF printf " ${G}✓${N} cursor ${D}→ $config${N}\n" } register_windsurf_devin() { local codedb_bin="$1" # Windsurf and Devin both use a standard mcpServers JSON object, so we register # codedb directly (additively, like the tools above) rather than through # mcpsync. Direct writes only touch the codedb entry — they can't drop a # server's nested env/headers — and add no external dependency. Each is # registered only when the tool is actually present. if ! command -v python3 >/dev/null 2>&1; then printf " ${D}windsurf/devin: skip (python3 not found)${N}\n" return fi if [ -d "$HOME/.codeium/windsurf" ]; then _register_json_mcp "$HOME/.codeium/windsurf/mcp_config.json" "$codedb_bin" "windsurf" fi if [ -d "$HOME/.config/devin" ]; then _register_json_mcp "$HOME/.config/devin/config.json" "$codedb_bin" "devin" fi } _register_json_mcp() { local config="$1" local codedb_bin="$2" local label="$3" _register_mcp "$config" "$codedb_bin" "$label" "mcpServers" } _client_present() { local dir="$1" shift if [ -n "$dir" ] && [ -d "$dir" ]; then return 0 fi local cmd for cmd in "$@"; do [ -n "$cmd" ] && command -v "$cmd" >/dev/null 2>&1 && return 0 done return 1 } # flavor: mcpServers | mcp.servers | opencode | copilot | hermes _register_mcp() { local config="$1" local codedb_bin="$2" local label="$3" local flavor="$4" local rc=0 python3 - "$config" "$codedb_bin" "$flavor" << 'PYEOF' || rc=$? import json, os, re, sys config_path, codedb_bin, flavor = sys.argv[1], sys.argv[2], sys.argv[3] def load_json(path): try: with open(path) as f: data = json.load(f) except FileNotFoundError: return {} except json.JSONDecodeError: sys.exit(4) return data if isinstance(data, dict) else {} def save_json(path, data): d = os.path.dirname(path) if d: os.makedirs(d, exist_ok=True) with open(path, "w") as f: json.dump(data, f, indent=2) f.write("\n") stdio = {"command": codedb_bin, "args": ["mcp"]} if flavor == "mcpServers": data = load_json(config_path) data.setdefault("mcpServers", {})["codedb"] = stdio save_json(config_path, data) elif flavor == "mcp.servers": data = load_json(config_path) data.setdefault("mcp", {}).setdefault("servers", {})["codedb"] = stdio save_json(config_path, data) elif flavor == "opencode": data = load_json(config_path) mcp = data.setdefault("mcp", {}) if not isinstance(mcp, dict): mcp = {} data["mcp"] = mcp entry = {"type": "local", "command": [codedb_bin, "mcp"], "enabled": True} if isinstance(mcp.get("servers"), dict): mcp["servers"]["codedb"] = entry else: mcp["codedb"] = entry save_json(config_path, data) elif flavor == "copilot": data = load_json(config_path) data.setdefault("mcpServers", {})["codedb"] = { "type": "local", "command": codedb_bin, "args": ["mcp"], "tools": ["*"], } save_json(config_path, data) elif flavor == "hermes": quoted = json.dumps(codedb_bin) entry = [" codedb:", f" command: {quoted}", ' args: ["mcp"]'] d = os.path.dirname(config_path) if d: os.makedirs(d, exist_ok=True) if not os.path.isfile(config_path) or os.path.getsize(config_path) == 0: with open(config_path, "w") as f: f.write("mcp_servers:\n" + "\n".join(entry) + "\n") sys.exit(0) with open(config_path) as f: raw = f.read() nl = "\r\n" if "\r\n" in raw else "\n" lines = raw.splitlines(keepends=True) def body(line): return line.split("#", 1)[0].rstrip("\r\n") def indent_of(line): s = body(line) if not s.strip(): return None return len(s) - len(s.lstrip(" ")) mcp_idx = None for i, line in enumerate(lines): if re.match(r"^mcp_servers:\s*$", body(line)): mcp_idx = i break if mcp_idx is None: for i, line in enumerate(lines): if re.match(r"^mcp_servers:\s+\S", body(line)): sys.exit(5) if raw and not raw.endswith(("\n", "\r\n")): raw += nl extra = "" if raw.endswith(nl) else nl with open(config_path, "w") as f: f.write(raw + extra + "mcp_servers:" + nl + nl.join(entry) + nl) sys.exit(0) codedb_start = None codedb_end = None i = mcp_idx + 1 while i < len(lines): ind = indent_of(lines[i]) if ind is None: i += 1 continue if ind == 0: break if ind == 2 and re.match(r"^\s{2}codedb:\s*", body(lines[i])): codedb_start = i j = i + 1 while j < len(lines): indj = indent_of(lines[j]) if indj is None: j += 1 continue if indj <= 2: break j += 1 codedb_end = j break i += 1 block = [e + nl for e in entry] if codedb_start is not None: new_lines = lines[:codedb_start] + block + lines[codedb_end:] else: new_lines = lines[: mcp_idx + 1] + block + lines[mcp_idx + 1 :] with open(config_path, "w") as f: f.writelines(new_lines) else: sys.exit(2) PYEOF case "$rc" in 0) printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" ;; 4) printf " ${Y}%-12s skip (invalid JSON) → %s${N}\n" "$label" "$config" ;; 5) printf " ${Y}%-12s skip (inline YAML mcp_servers) → %s${N}\n" "$label" "$config" ;; *) printf " ${Y}%-12s registration failed → %s${N}\n" "$label" "$config" ;; esac return 0 } register_detected_clients() { local codedb_bin="$1" if ! command -v python3 >/dev/null 2>&1; then printf " ${D}extra clients: skip (python3 not found)${N}\n" return fi if _client_present "$HOME/.omp" omp; then _register_mcp "$HOME/.omp/agent/mcp.json" "$codedb_bin" "oh-my-pi" "mcpServers" fi if _client_present "$HOME/.hermes" hermes; then _register_mcp "$HOME/.hermes/config.yaml" "$codedb_bin" "hermes" "hermes" fi if _client_present "$HOME/.qwen" qwen; then _register_mcp "$HOME/.qwen/settings.json" "$codedb_bin" "qwen-code" "mcpServers" fi if _client_present "$HOME/.zcode" zcode; then _register_mcp "$HOME/.zcode/cli/config.json" "$codedb_bin" "zcode" "mcp.servers" fi if _client_present "$HOME/.trae" trae traecli; then _register_mcp "$HOME/.trae/mcp.json" "$codedb_bin" "trae" "mcpServers" fi local trae_root trae_cfg for trae_root in \ "$HOME/Library/Application Support/Trae" \ "$HOME/Library/Application Support/Trae CN" \ "$HOME/AppData/Roaming/Trae" \ "$HOME/AppData/Roaming/Trae CN" do [ -d "$trae_root" ] || continue if [ -d "$trae_root/User/settings" ] || [ -f "$trae_root/User/settings/mcp.json" ]; then trae_cfg="$trae_root/User/settings/mcp.json" elif [ -d "$trae_root/User" ] || [ -f "$trae_root/User/mcp.json" ]; then trae_cfg="$trae_root/User/mcp.json" else continue fi _register_mcp "$trae_cfg" "$codedb_bin" "trae" "mcpServers" done if _client_present "$HOME/.cline" cline; then _register_mcp "$HOME/.cline/data/settings/cline_mcp_settings.json" "$codedb_bin" "cline" "mcpServers" fi local cline_cfg cline_ext for cline_cfg in \ "$HOME/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ "$HOME/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ "$HOME/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ "$HOME/AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ "$HOME/AppData/Roaming/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" do cline_ext="$(dirname "$(dirname "$cline_cfg")")" if [ -d "$cline_ext" ]; then _register_mcp "$cline_cfg" "$codedb_bin" "cline" "mcpServers" fi done if _client_present "$HOME/.copilot" copilot; then _register_mcp "$HOME/.copilot/mcp-config.json" "$codedb_bin" "copilot" "copilot" fi if _client_present "$HOME/.gemini/antigravity" agy antigravity; then # Antigravity's own config — not Gemini CLI's ~/.gemini/settings.json # and not ~/.gemini/config/mcp_config.json. _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" fi if _client_present "$HOME/.kiro" kiro kiro-cli; then _register_mcp "$HOME/.kiro/settings/mcp.json" "$codedb_bin" "kiro" "mcpServers" fi if _client_present "$HOME/.config/opencode" opencode; then if [ -f "$HOME/.config/opencode/opencode.jsonc" ] && [ ! -f "$HOME/.config/opencode/opencode.json" ]; then printf " ${Y}%-12s skip (opencode.jsonc is not rewritten) → %s${N}\n" \ "opencode" "$HOME/.config/opencode/opencode.jsonc" else _register_mcp "$HOME/.config/opencode/opencode.json" "$codedb_bin" "opencode" "opencode" fi fi } DEEPWIKI_URL="https://mcp.deepwiki.com/mcp" _register_deepwiki_json() { local config="$1" local label="$2" local entry_json="$3" local rc=0 python3 - "$config" "$entry_json" << 'PYEOF' || rc=$? import json, sys, os config_path, entry_json = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) except FileNotFoundError: data = {} except json.JSONDecodeError: sys.exit(4) # never clobber a malformed file if not isinstance(data, dict): sys.exit(4) servers = data.setdefault("mcpServers", {}) if "deepwiki" in servers: sys.exit(3) # already configured — never clobber a user's own entry servers["deepwiki"] = json.loads(entry_json) d = os.path.dirname(config_path) if d: os.makedirs(d, exist_ok=True) with open(config_path, "w") as f: json.dump(data, f, indent=2) f.write("\n") PYEOF case "$rc" in 0) printf " ${G}✓${N} %-12s ${D}→ %s (+deepwiki)${N}\n" "$label" "$config" ;; 3) printf " ${D}%-12s deepwiki already configured → %s${N}\n" "$label" "$config" ;; 4) printf " ${Y}%-12s skip deepwiki (invalid JSON) → %s${N}\n" "$label" "$config" ;; *) printf " ${Y}%-12s deepwiki registration failed → %s${N}\n" "$label" "$config" ;; esac return 0 } register_deepwiki() { # DeepWiki is a free, no-auth REMOTE MCP server (mcp.deepwiki.com) that # answers questions about public GitHub repos — a good complement to codedb's # local index. Registered additively alongside codedb; an existing "deepwiki" # entry is never overwritten. Set CODEDB_INSTALL_DEEPWIKI=0 to opt out. # Note: text sent to its tools (e.g. ask_question) leaves the machine. if [ "${CODEDB_INSTALL_DEEPWIKI:-1}" = "0" ]; then printf " ${D}deepwiki: skip (CODEDB_INSTALL_DEEPWIKI=0)${N}\n" return fi if ! command -v python3 >/dev/null 2>&1; then printf " ${D}deepwiki: skip (python3 not found)${N}\n" return fi # Claude Code: always (mirrors register_claude). Field: type+url. _register_deepwiki_json "$HOME/.claude.json" "claude code" \ "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\"}" # Gemini CLI: streamable HTTP uses httpUrl. if [ -d "$HOME/.gemini" ]; then _register_deepwiki_json "$HOME/.gemini/settings.json" "gemini cli" \ "{\"httpUrl\":\"$DEEPWIKI_URL\"}" fi # Cursor: standard url field. if [ -d "$HOME/.cursor" ]; then _register_deepwiki_json "$HOME/.cursor/mcp.json" "cursor" \ "{\"url\":\"$DEEPWIKI_URL\"}" fi # Windsurf and Devin: both use serverUrl. if [ -d "$HOME/.codeium/windsurf" ]; then _register_deepwiki_json "$HOME/.codeium/windsurf/mcp_config.json" "windsurf" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.config/devin" ]; then _register_deepwiki_json "$HOME/.config/devin/config.json" "devin" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi # Detected extra clients that use a standard mcpServers JSON object. if [ -d "$HOME/.omp" ] || command -v omp >/dev/null 2>&1; then _register_deepwiki_json "$HOME/.omp/agent/mcp.json" "oh-my-pi" \ "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.qwen" ] || command -v qwen >/dev/null 2>&1; then _register_deepwiki_json "$HOME/.qwen/settings.json" "qwen-code" \ "{\"httpUrl\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.trae" ] || command -v trae >/dev/null 2>&1 || command -v traecli >/dev/null 2>&1; then _register_deepwiki_json "$HOME/.trae/mcp.json" "trae" \ "{\"url\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.cline" ] || command -v cline >/dev/null 2>&1; then _register_deepwiki_json "$HOME/.cline/data/settings/cline_mcp_settings.json" "cline" \ "{\"type\":\"streamableHttp\",\"url\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.copilot" ] || command -v copilot >/dev/null 2>&1; then _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" fi if _client_present "$HOME/.gemini/antigravity" agy antigravity; then _register_deepwiki_json "$HOME/.gemini/antigravity/mcp_config.json" "antigravity" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then _register_deepwiki_json "$HOME/.kiro/settings/mcp.json" "kiro" \ "{\"url\":\"$DEEPWIKI_URL\"}" fi # Codex: TOML, url key selects the streamable-HTTP transport. local codex_cfg="$HOME/.codex/config.toml" if grep -q '\[mcp_servers\.deepwiki\]' "$codex_cfg" 2>/dev/null; then printf " ${D}%-12s deepwiki already configured → %s${N}\n" "codex" "$codex_cfg" else mkdir -p "$HOME/.codex" { [ -f "$codex_cfg" ] && [ -s "$codex_cfg" ] && echo "" || true echo '[mcp_servers.deepwiki]' echo "url = \"$DEEPWIKI_URL\"" } >> "$codex_cfg" printf " ${G}✓${N} %-12s ${D}→ %s (+deepwiki)${N}\n" "codex" "$codex_cfg" fi printf " ${D}note: deepwiki is a third-party remote service — queries sent to its tools leave this machine${N}\n" } register_hooks() { if ! command -v python3 >/dev/null 2>&1; then printf " ${D}hooks: skip (python3 not found)${N}\n" return fi python3 << 'PYEOF' import json, os, stat home = os.path.expanduser("~") hooks_dir = os.path.join(home, ".claude", "hooks") settings_path = os.path.join(home, ".claude", "settings.json") os.makedirs(hooks_dir, exist_ok=True) # Opt-out: CODEDB_NO_HOOKS=1 skips (re-)registering the PreToolUse # block-legacy hook for THIS run only. It is deliberately NOT persisted: the # background auto-updater re-runs this installer with the environment it # inherited from the codedb process (src/update.zig), so a transient export # must never become a permanent on-disk opt-out. Persist it explicitly with # CODEDB_PERSIST_NO_HOOKS=1 or `touch ~/.codedb/no-hooks`; `rm` that file to # re-enable. A deliberate removal from settings.json also persists (#658). no_hooks_marker = os.path.join(home, ".codedb", "no-hooks") def persist_no_hooks(): os.makedirs(os.path.dirname(no_hooks_marker), exist_ok=True) with open(no_hooks_marker, "w") as f: f.write("") skip_pretooluse = bool(os.environ.get("CODEDB_NO_HOOKS")) or os.path.exists(no_hooks_marker) if os.environ.get("CODEDB_PERSIST_NO_HOOKS") and not os.path.exists(no_hooks_marker): persist_no_hooks() skip_pretooluse = True try: with open(settings_path) as f: data = json.load(f) except (FileNotFoundError, json.JSONDecodeError): data = {} hooks = data.setdefault("hooks", {}) # #658: honor a deliberate removal. If a previous run registered the # PreToolUse hook (receipt exists) but its settings.json entry is gone, the # user deleted it — persist the opt-out marker instead of re-adding the hook # on the next unattended auto-update run. This runs BEFORE the scripts are # written so the removed hook script does not reappear on disk either. hooks_receipt = os.path.join(home, ".codedb", "hooks-registered") def pretooluse_entry_present(): for e in hooks.get("PreToolUse", []): for h in e.get("hooks", []): if "codedb-block-legacy.sh" in h.get("command", ""): return True return False if not skip_pretooluse and os.path.exists(hooks_receipt) and not pretooluse_entry_present(): persist_no_hooks() skip_pretooluse = True scripts = { "codedb-block-legacy.sh": r'''#!/bin/bash # codedb PreToolUse guard. Nudges agents from native file tools to codedb — # but ONLY inside a codedb-indexed repo, and never for paths outside it. # Fail-open by design (a nudge, not a wall). Disable entirely: CODEDB_NO_HOOKS=1. [ -n "$CODEDB_NO_HOOKS" ] && exit 0 [ -f "$HOME/.codedb/no-hooks" ] && exit 0 command -v jq >/dev/null 2>&1 || exit 0 command -v codedb >/dev/null 2>&1 || exit 0 INPUT=$(cat) CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') [ -z "$CMD" ] && exit 0 STRIPPED=$(echo "$CMD" | sed -E 's/^[[:space:]]*(env|sudo|command|builtin|exec|nohup)[[:space:]]+//') STRIPPED=$(echo "$STRIPPED" | sed -E 's/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+//') FIRST=$(echo "$STRIPPED" | awk '{print $1}') case "$FIRST" in grep|rg|egrep|fgrep|cat|head|tail|find) ;; *) exit 0 ;; esac # Scope 1: cwd must sit at/under a codedb-indexed root. Otherwise codedb has # nothing to offer here — allow the native tool (no blocking outside repos, on # unindexed dirs, or in ~/.claude). PWD_ABS=$(pwd -P) REPO_ROOT="" for pt in "$HOME"/.codedb/projects/*/project.txt; do [ -f "$pt" ] || continue root=$(head -1 "$pt" 2>/dev/null) [ -z "$root" ] && continue [ "$root" = "/" ] && continue # degenerate root matches everything [ "$root" = "$HOME" ] && continue # home indexed as a project would block all of ~ if [ "$PWD_ABS" = "$root" ] || [ "${PWD_ABS#"$root"/}" != "$PWD_ABS" ]; then REPO_ROOT="$root"; break fi done [ -z "$REPO_ROOT" ] && exit 0 # Scope 2: if the command names an ABSOLUTE path outside this repo (cat /etc/hosts, # grep x /tmp/f), codedb can't read it — allow. Relative paths are assumed # in-repo (cwd is in-repo). Fail-open: any out-of-repo absolute arg -> allow. set -f for tok in $STRIPPED; do case "$tok" in /*) if [ "$tok" = "$REPO_ROOT" ] || [ "${tok#"$REPO_ROOT"/}" != "$tok" ]; then :; else set +f; exit 0; fi ;; esac done set +f case "$FIRST" in grep|rg|egrep|fgrep) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_search \"\" (codedb_word for an exact identifier, codedb_callers for call sites) instead of $FIRST — ranked + fewer tokens. Native $FIRST is allowed outside this repo or with CODEDB_NO_HOOKS=1." >&2; exit 2 ;; cat) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= (codedb_outline first for a map) instead of cat. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; head|tail) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= line_start=.. line_end=.. instead of $FIRST. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; find) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_find (fuzzy names) or codedb_glob (patterns) instead of find. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; esac exit 0 ''', "codedb-warmup.sh": r'''#!/bin/bash command -v codedb >/dev/null 2>&1 || exit 0 codedb . status >/dev/null 2>&1 & exit 0 ''', } for name, content in scripts.items(): if name == "codedb-block-legacy.sh" and skip_pretooluse: continue path = os.path.join(hooks_dir, name) with open(path, "w") as f: f.write(content) os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) # Merge codedb hooks without clobbering existing hooks from other tools. # If a competing legacy-tools hook is already registered for the same # event/matcher (e.g. muonry's block-legacy-tools.sh), insert codedb's # entry at the FRONT of the list so its redirect wins the race; otherwise # append. Re-runs will also reshuffle an already-registered codedb hook # to the front if a competitor has appeared since the previous install. COMPETITOR_MARKERS = ("block-legacy-tools", "muonry", "zigrep", "zigread") def merge_hook(event, new_entry): existing = hooks.get(event, []) cmd = new_entry["hooks"][0]["command"] matcher = new_entry.get("matcher", "") competes = any( e.get("matcher", "") == matcher and any(any(m in h.get("command", "") for m in COMPETITOR_MARKERS) for h in e.get("hooks", [])) for e in existing ) idx = None for i, e in enumerate(existing): if any(cmd in h.get("command", "") for h in e.get("hooks", [])): idx = i break if idx is not None: if competes and idx != 0: existing.insert(0, existing.pop(idx)) hooks[event] = existing return if competes: existing.insert(0, new_entry) else: existing.append(new_entry) hooks[event] = existing if not skip_pretooluse: merge_hook("PreToolUse", {"matcher": "Bash", "hooks": [{"type": "command", "command": "$HOME/.claude/hooks/codedb-block-legacy.sh"}]}) os.makedirs(os.path.dirname(hooks_receipt), exist_ok=True) with open(hooks_receipt, "w") as f: f.write("") merge_hook("SessionStart", {"matcher": "", "hooks": [{"type": "command", "command": "$HOME/.claude/hooks/codedb-warmup.sh"}]}) # Auto-allow codedb's own MCP tools so callers aren't prompted for every # codedb_* call. Purely additive — we add only the codedb-scoped rule and # never touch other servers' permissions. The "mcp__codedb__*" form (literal # server prefix + tool glob) is the syntax Claude Code's permission validator # accepts; a bare "mcp__*" is rejected and silently skipped. perms = data.setdefault("permissions", {}) allow = perms.setdefault("allow", []) if isinstance(allow, list) and "mcp__codedb__*" not in allow: allow.append("mcp__codedb__*") with open(settings_path, "w") as f: json.dump(data, f, indent=2) f.write("\n") PYEOF printf " ${G}✓${N} hooks ${D}→ ~/.claude/hooks/ + settings.json${N}\n" } print_hook_notes() { local codedb_bin="$1" echo "" printf " ${W}mcp command${N}\n" printf " ${C}$codedb_bin mcp${N}\n" } main() { local platform version ext="" platform="$(detect_platform)" # #677: detect_platform runs in a command substitution, so it cannot print # to the user or stop the script itself — handle Windows here, before any # download is attempted. if [ "$platform" = "windows" ]; then echo "" printf " ${W}codedb installer${N}\n" echo "" printf " ${Y}Windows detected${N} — codedb has a native Windows x86_64 binary.\n" printf " This Bash installer is for macOS/Linux. Run this in PowerShell:\n" echo "" printf " ${C}irm https://raw.githubusercontent.com/justrach/codedb/v0.2.5833/install/install.ps1 | iex${N}\n" echo "" printf " Use ${G}WSL2${N} only if you want the Linux binary inside WSL.\n" echo "" exit 0 fi echo "" printf " ${W}codedb${N} ${D}installer${N}\n" echo "" printf " ${D}platform${N} $platform\n" version="${CODEDB_VERSION:-}" if [ -z "$version" ]; then version="$(fetch_latest_version)" fi if [ -z "$version" ]; then printf " ${R}error: could not fetch latest version${N}\n" >&2 exit 1 fi printf " ${D}version${N} v${version}\n" [[ "$platform" == windows-* ]] && ext=".exe" mkdir -p "$INSTALL_DIR" printf " ${D}install${N} $INSTALL_DIR\n" echo "" local url="https://github.com/justrach/codedb/releases/download/v${version}/codedb-${platform}${ext}" local checksum_url="https://github.com/justrach/codedb/releases/download/v${version}/checksums.sha256" local dest="$INSTALL_DIR/codedb${ext}" printf " ${D}│${N} %-12s " "codedb" local tmp tmp="$(mktemp "$INSTALL_DIR/.codedb-download.XXXXXX")" trap 'rm -f "$tmp"' EXIT if curl -fsSL -A 'codedb-installer' "$url" -o "$tmp" 2>/dev/null; then # Keep any existing binary until the downloaded bytes are verified. local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; if (NF != 2) malformed=1; hash=$1 } END { if (count == 1 && !malformed) print hash }')" if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual_hash="$(shasum -a 256 "$tmp" | awk '{print $1}')" fi if [ -z "$actual_hash" ]; then printf "${R}failed${N}\n" printf "\n ${R}error: cannot verify download — install sha256sum or shasum first. Existing binary unchanged.${N}\n" >&2 exit 1 elif [ "$actual_hash" != "$expected_hash" ]; then rm -f "$tmp" printf "${R}failed${N}\n" printf "\n ${R}error: checksum mismatch — binary may be corrupted${N}\n" >&2 printf " ${D}expected: $expected_hash${N}\n" >&2 printf " ${D}actual: $actual_hash${N}\n" >&2 exit 1 fi else printf "${R}failed${N}\n" printf "\n ${R}error: no unique valid checksum for this platform. Existing binary unchanged; retry when release checksums are available.${N}\n" >&2 exit 1 fi xattr -c "$tmp" 2>/dev/null || true mv -f "$tmp" "$dest" trap - EXIT chmod +x "$dest" printf "${G}✓${N}\n" else printf "${R}failed${N}\n" printf "\n ${R}error: download failed${N}\n" >&2 printf " ${D}url: $url${N}\n" >&2 exit 1 fi echo "" printf " ${G}installed${N} ${D}→ $dest${N}\n" printf " ${D}claude hook opt-out: CODEDB_NO_HOOKS=1 skips this run; CODEDB_PERSIST_NO_HOOKS=1 or touch ~/.codedb/no-hooks makes it permanent (rm ~/.codedb/no-hooks re-enables)${N}\n" # Register MCP server in coding tools echo "" printf " ${W}registering integrations${N}\n" echo "" if [ -z "${CODEDB_NO_INTEGRATIONS:-}" ]; then register_claude "$dest" register_codex "$dest" register_codex_policy "$dest" register_gemini "$dest" register_cursor "$dest" register_windsurf_devin "$dest" register_detected_clients "$dest" register_deepwiki register_hooks else printf " ${D}skipped (CODEDB_NO_INTEGRATIONS)${N}\n" fi print_hook_notes "$dest" # Check PATH case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) echo "" printf " ${Y}add to PATH:${N}\n" printf " ${C}export PATH=\"$INSTALL_DIR:\$PATH\"${N}\n" printf " ${D}(add to ~/.bashrc or ~/.zshrc)${N}\n" ;; esac echo "" printf " ${W}done!${N} run ${C}codedb --help${N} to get started\n" echo "" } main