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/.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 416f6dc..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" @@ -135,9 +140,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: 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 } ] 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