From e212b554bd437bc61ae092fa30eb43df51bc10f9 Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Mon, 1 Jun 2026 14:26:15 -0500 Subject: [PATCH 1/3] fix: run hooks through a long-path-safe bootstrap so they work on Windows The PreToolUse and PostToolUse hooks passed the full ${CLAUDE_PLUGIN_ROOT} script path to python3. On Windows that path lives in a deeply nested plugin cache and can go past the 260 character MAX_PATH limit, so python could not open the script and errored on every matched edit or SQL call. The hooks now run a small inline bootstrap that loads the script with runpy and, on Windows, prefixes the path with the \\?\ long-path escape that gets around MAX_PATH. The bootstrap has no long path to open itself, the existing scripts stay the single source of truth, and the trailing "; exit 0" keeps a failed bootstrap from interrupting editing. Fixes #20 --- hooks/hooks.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index 80c8003..95be03b 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/validate-sql.py\"", + "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/validate-sql.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0", "timeout": 10 } ] @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/check-sql-files.py\"", + "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/check-sql-files.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0", "timeout": 15 } ] From 9887a715043a3902e85acc97351abf0e5333f476 Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Mon, 1 Jun 2026 14:26:15 -0500 Subject: [PATCH 2/3] docs: document the long-path hook pattern and drop hardcoded counts Update the CONTRIBUTING hook guidance to show the long-path-safe bootstrap so new hooks do not bring the Windows MAX_PATH problem back. Add a Windows note to the README that explains hooks need python3 on PATH and that long-path support is not required. Also remove the fixed backend and agent counts from the plugin description, marketplace entry, and README intro so they do not go stale as the plugin grows. --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- CONTRIBUTING.md | 4 ++-- README.md | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 79a4dbe..8bf0209 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ "plugins": [ { "name": "cockroachdb", - "description": "Connect Claude Code directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides tools across two active MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), three specialized agents (DBA, Developer, Operator), skills across multiple operational domains, and built-in safety hooks.", + "description": "Connect Claude Code directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides tools across MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), specialized agents (DBA, Developer, Operator), skills across operational domains, and built-in safety hooks.", "source": "./" } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 81d4a1a..a0469af 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "cockroachdb", "version": "0.1.9", - "description": "Connect Claude Code directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides tools across two active MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), three specialized agents (DBA, Developer, Operator), skills across multiple operational domains, and built-in safety hooks.", + "description": "Connect Claude Code directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides tools across MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), specialized agents (DBA, Developer, Operator), skills across operational domains, and built-in safety hooks.", "author": { "name": "Cockroach Labs", "url": "https://github.com/cockroachdb" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 416f6dc..f5b71d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,9 +135,9 @@ This repo uses [Release Please](https://github.com/googleapis/release-please) fo - Hook scripts must be Python 3 with **no external dependencies** (stdlib only). - Read JSON from stdin, write JSON to stdout. - Exit code 0 = allow/continue; exit code 2 = block the tool call. -- Always quote `${CLAUDE_PLUGIN_ROOT}` in `hooks.json` commands to handle paths with spaces: +- Load hook scripts through the long-path-safe bootstrap below instead of passing the script path straight to `python3`. On Windows, `${CLAUDE_PLUGIN_ROOT}` resolves to a deeply nested cache path that can exceed the 260-character `MAX_PATH` limit; passing the path directly makes Python fail to open the script and error on every matched tool call (see issue #20). The bootstrap loads the script with `runpy`, prefixing the path with the `\\?\` long-path escape on Windows, keeps it inside single quotes so paths with spaces still work, and uses `; exit 0` so a failed bootstrap never disrupts editing: ```json - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/your-script.py\"" + "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/your-script.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0" ``` ### MCP Configuration diff --git a/README.md b/README.md index d63424b..4d819a5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Release Please](https://github.com/cockroachdb/claude-plugin/actions/workflows/release-please.yml/badge.svg)](https://github.com/cockroachdb/claude-plugin/actions/workflows/release-please.yml) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -Connect [Claude Code](https://code.claude.com/) directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides tools across two active MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), three specialized agents (DBA, Developer, Operator), skills across multiple operational domains, and built-in safety hooks. +Connect [Claude Code](https://code.claude.com/) directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides tools across MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), specialized agents (DBA, Developer, Operator), skills across operational domains, and built-in safety hooks. ## Installation @@ -236,6 +236,8 @@ Agents are auto-discovered from the `agents/` directory. Claude invokes them aut Hooks run as Python scripts (Python 3, no external dependencies) and provide automated safety guardrails. +**Windows note:** the hooks invoke `python3`, so make sure a `python3` is on your `PATH`. The python.org installer creates `python.exe` and the `py` launcher but **not** `python3.exe`; on those installs the hooks safely no-op (they never block editing, but the safety checks won't run). Installing Python from the Microsoft Store — or adding a `python3` alias — enables them. You do **not** need to turn on Windows long-path support: the hooks load their scripts through the `\\?\` long-path prefix, so they work no matter how deep the plugin cache path is. + ## Development Clone the repository: From fdc4747c4cb474ec0e33a906491edd097b7dc8be Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Tue, 30 Jun 2026 16:46:26 -0400 Subject: [PATCH 3/3] test: add hook regression suite and CI for the fail-open behavior Covers issue #20 (resolved path exceeds Windows MAX_PATH) and issue #23 (${CLAUDE_PLUGIN_ROOT} not substituted by the host): in both cases the interpreter cannot open the script, and the hook must exit 0 with no user-visible block so editing is never interrupted. Also asserts the positive deny/warn/lint cases. Runs in CI on changes to hooks or scripts. --- .github/workflows/test-hooks.yml | 26 ++++++++++++ CONTRIBUTING.md | 5 +++ scripts/test-hooks.sh | 73 ++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 .github/workflows/test-hooks.yml create mode 100755 scripts/test-hooks.sh diff --git a/.github/workflows/test-hooks.yml b/.github/workflows/test-hooks.yml new file mode 100644 index 0000000..f8b90e3 --- /dev/null +++ b/.github/workflows/test-hooks.yml @@ -0,0 +1,26 @@ +name: Test hooks + +on: + pull_request: + paths: + - 'hooks/**' + - 'scripts/**' + - '.github/workflows/test-hooks.yml' + push: + branches: + - main + paths: + - 'hooks/**' + - 'scripts/**' + - '.github/workflows/test-hooks.yml' + +permissions: + contents: read + +jobs: + test-hooks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run hook regression tests + run: bash scripts/test-hooks.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5b71d6..75cdc7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,6 +97,11 @@ submodules/ echo '{"tool_input":{"file_path":"test.sql"}}' | python3 scripts/check-sql-files.py ``` + Run the full hook regression suite (also runs in CI): + ```bash + bash scripts/test-hooks.sh + ``` + 5. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/): ```bash git commit -m "fix: quote CLAUDE_PLUGIN_ROOT for paths with spaces" diff --git a/scripts/test-hooks.sh b/scripts/test-hooks.sh new file mode 100755 index 0000000..a714b98 --- /dev/null +++ b/scripts/test-hooks.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Regression tests for the CockroachDB safety hooks. +# +# Runs the actual commands from hooks/hooks.json (with ${CLAUDE_PLUGIN_ROOT} +# substituted) and asserts the contract: +# +# - dangerous SQL is blocked (PreToolUse permissionDecision: deny) +# - anti-patterns emit a warning (systemMessage) +# - safe SQL and non-SQL file edits produce no user-visible block +# - a missing or UNSUBSTITUTED plugin root fails open: exit 0, no block +# +# The last case is the regression for issue #20 (path exceeds MAX_PATH) and +# issue #23 (${CLAUDE_PLUGIN_ROOT} not substituted by the host). In both, the +# interpreter cannot open the script; the hook must still exit 0 so the editor +# is never interrupted. +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOOKS="$ROOT/hooks/hooks.json" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +fails=0 + +# Extract a hook command from hooks.json and substitute the plugin root token. +hook_cmd() { # $1=event $2=root + python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["hooks"][sys.argv[2]][0]["hooks"][0]["command"].replace("${CLAUDE_PLUGIN_ROOT}", sys.argv[3]))' "$HOOKS" "$1" "$2" +} + +# check: desc, event, root, stdin, expected_rc, mode(empty|contains), substr +check() { + local desc="$1" event="$2" root="$3" stdin="$4" want_rc="$5" mode="$6" substr="${7:-}" + local cmd out rc ok=1 + cmd="$(hook_cmd "$event" "$root")" + out="$(printf '%s' "$stdin" | sh -c "$cmd" 2>/dev/null)"; rc=$? + [ "$rc" = "$want_rc" ] || ok=0 + case "$mode" in + empty) [ -z "$out" ] || ok=0 ;; + contains) printf '%s' "$out" | grep -q "$substr" || ok=0 ;; + esac + if [ "$ok" = 1 ]; then + echo "ok - $desc" + else + echo "FAIL - $desc (rc=$rc, out=${out:-})" + fails=$((fails + 1)) + fi +} + +printf 'CREATE TABLE t (id SERIAL PRIMARY KEY);\n' > "$TMP/a.sql" +printf '# just markdown, not sql\n' > "$TMP/a.md" + +# --- PreToolUse (validate-sql.py), plugin root resolved --- +check "blocks DROP DATABASE" PreToolUse "$ROOT" '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 contains '"permissionDecision": "deny"' +check "blocks TRUNCATE" PreToolUse "$ROOT" '{"tool_input":{"sql":"TRUNCATE TABLE t"}}' 0 contains '"permissionDecision": "deny"' +check "warns on SERIAL" PreToolUse "$ROOT" '{"tool_input":{"sql":"CREATE TABLE t (id SERIAL)"}}' 0 contains 'systemMessage' +check "safe SQL produces no block" PreToolUse "$ROOT" '{"tool_input":{"sql":"SELECT 1"}}' 0 empty + +# --- PostToolUse (check-sql-files.py), plugin root resolved --- +check "lints SERIAL in a .sql file" PostToolUse "$ROOT" "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 contains 'CockroachDB lint' +check "non-SQL edit produces no block" PostToolUse "$ROOT" "{\"tool_input\":{\"file_path\":\"$TMP/a.md\"}}" 0 empty + +# --- regression: plugin root NOT substituted / missing (issues #20, #23) --- +# Pass the literal token as the "root" so the substitution is a no-op, exactly +# reproducing a host that does not expand ${CLAUDE_PLUGIN_ROOT}. +check "PreToolUse fails open on unsubstituted root" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 empty +check "PostToolUse fails open on unsubstituted root" PostToolUse '${CLAUDE_PLUGIN_ROOT}' "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 empty + +echo +if [ "$fails" -eq 0 ]; then + echo "All hook regression tests passed." +else + echo "$fails test(s) failed." + exit 1 +fi