Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "./"
}
]
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/test-hooks.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -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
}
]
Expand Down
73 changes: 73 additions & 0 deletions scripts/test-hooks.sh
Original file line number Diff line number Diff line change
@@ -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:-<empty>})"
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
Loading