Skip to content

Commit 81f29eb

Browse files
fix: make hooks work on Windows by loading scripts long-path-safe (#21)
Fixes #20. Fixes #23.
1 parent 736bd11 commit 81f29eb

7 files changed

Lines changed: 113 additions & 7 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"plugins": [
1212
{
1313
"name": "cockroachdb",
14-
"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.",
14+
"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.",
1515
"source": "./"
1616
}
1717
]

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cockroachdb",
33
"version": "0.1.9",
4-
"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.",
4+
"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.",
55
"author": {
66
"name": "Cockroach Labs",
77
"url": "https://github.com/cockroachdb"

.github/workflows/test-hooks.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Test hooks
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'hooks/**'
7+
- 'scripts/**'
8+
- '.github/workflows/test-hooks.yml'
9+
push:
10+
branches:
11+
- main
12+
paths:
13+
- 'hooks/**'
14+
- 'scripts/**'
15+
- '.github/workflows/test-hooks.yml'
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
test-hooks:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- name: Run hook regression tests
26+
run: bash scripts/test-hooks.sh

CONTRIBUTING.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ submodules/
9797
echo '{"tool_input":{"file_path":"test.sql"}}' | python3 scripts/check-sql-files.py
9898
```
9999

100+
Run the full hook regression suite (also runs in CI):
101+
```bash
102+
bash scripts/test-hooks.sh
103+
```
104+
100105
5. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/):
101106
```bash
102107
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
135140
- Hook scripts must be Python 3 with **no external dependencies** (stdlib only).
136141
- Read JSON from stdin, write JSON to stdout.
137142
- Exit code 0 = allow/continue; exit code 2 = block the tool call.
138-
- Always quote `${CLAUDE_PLUGIN_ROOT}` in `hooks.json` commands to handle paths with spaces:
143+
- 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:
139144
```json
140-
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/your-script.py\""
145+
"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"
141146
```
142147

143148
### MCP Configuration

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![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)
44
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
55

6-
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.
6+
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.
77

88
## Installation
99

@@ -236,6 +236,8 @@ Agents are auto-discovered from the `agents/` directory. Claude invokes them aut
236236

237237
Hooks run as Python scripts (Python 3, no external dependencies) and provide automated safety guardrails.
238238

239+
**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.
240+
239241
## Development
240242

241243
Clone the repository:

hooks/hooks.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"hooks": [
77
{
88
"type": "command",
9-
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/validate-sql.py\"",
9+
"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",
1010
"timeout": 10
1111
}
1212
]
@@ -18,7 +18,7 @@
1818
"hooks": [
1919
{
2020
"type": "command",
21-
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/check-sql-files.py\"",
21+
"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",
2222
"timeout": 15
2323
}
2424
]

scripts/test-hooks.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env bash
2+
# Regression tests for the CockroachDB safety hooks.
3+
#
4+
# Runs the actual commands from hooks/hooks.json (with ${CLAUDE_PLUGIN_ROOT}
5+
# substituted) and asserts the contract:
6+
#
7+
# - dangerous SQL is blocked (PreToolUse permissionDecision: deny)
8+
# - anti-patterns emit a warning (systemMessage)
9+
# - safe SQL and non-SQL file edits produce no user-visible block
10+
# - a missing or UNSUBSTITUTED plugin root fails open: exit 0, no block
11+
#
12+
# The last case is the regression for issue #20 (path exceeds MAX_PATH) and
13+
# issue #23 (${CLAUDE_PLUGIN_ROOT} not substituted by the host). In both, the
14+
# interpreter cannot open the script; the hook must still exit 0 so the editor
15+
# is never interrupted.
16+
set -uo pipefail
17+
18+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19+
HOOKS="$ROOT/hooks/hooks.json"
20+
TMP="$(mktemp -d)"
21+
trap 'rm -rf "$TMP"' EXIT
22+
fails=0
23+
24+
# Extract a hook command from hooks.json and substitute the plugin root token.
25+
hook_cmd() { # $1=event $2=root
26+
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"
27+
}
28+
29+
# check: desc, event, root, stdin, expected_rc, mode(empty|contains), substr
30+
check() {
31+
local desc="$1" event="$2" root="$3" stdin="$4" want_rc="$5" mode="$6" substr="${7:-}"
32+
local cmd out rc ok=1
33+
cmd="$(hook_cmd "$event" "$root")"
34+
out="$(printf '%s' "$stdin" | sh -c "$cmd" 2>/dev/null)"; rc=$?
35+
[ "$rc" = "$want_rc" ] || ok=0
36+
case "$mode" in
37+
empty) [ -z "$out" ] || ok=0 ;;
38+
contains) printf '%s' "$out" | grep -q "$substr" || ok=0 ;;
39+
esac
40+
if [ "$ok" = 1 ]; then
41+
echo "ok - $desc"
42+
else
43+
echo "FAIL - $desc (rc=$rc, out=${out:-<empty>})"
44+
fails=$((fails + 1))
45+
fi
46+
}
47+
48+
printf 'CREATE TABLE t (id SERIAL PRIMARY KEY);\n' > "$TMP/a.sql"
49+
printf '# just markdown, not sql\n' > "$TMP/a.md"
50+
51+
# --- PreToolUse (validate-sql.py), plugin root resolved ---
52+
check "blocks DROP DATABASE" PreToolUse "$ROOT" '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 contains '"permissionDecision": "deny"'
53+
check "blocks TRUNCATE" PreToolUse "$ROOT" '{"tool_input":{"sql":"TRUNCATE TABLE t"}}' 0 contains '"permissionDecision": "deny"'
54+
check "warns on SERIAL" PreToolUse "$ROOT" '{"tool_input":{"sql":"CREATE TABLE t (id SERIAL)"}}' 0 contains 'systemMessage'
55+
check "safe SQL produces no block" PreToolUse "$ROOT" '{"tool_input":{"sql":"SELECT 1"}}' 0 empty
56+
57+
# --- PostToolUse (check-sql-files.py), plugin root resolved ---
58+
check "lints SERIAL in a .sql file" PostToolUse "$ROOT" "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 contains 'CockroachDB lint'
59+
check "non-SQL edit produces no block" PostToolUse "$ROOT" "{\"tool_input\":{\"file_path\":\"$TMP/a.md\"}}" 0 empty
60+
61+
# --- regression: plugin root NOT substituted / missing (issues #20, #23) ---
62+
# Pass the literal token as the "root" so the substitution is a no-op, exactly
63+
# reproducing a host that does not expand ${CLAUDE_PLUGIN_ROOT}.
64+
check "PreToolUse fails open on unsubstituted root" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 empty
65+
check "PostToolUse fails open on unsubstituted root" PostToolUse '${CLAUDE_PLUGIN_ROOT}' "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 empty
66+
67+
echo
68+
if [ "$fails" -eq 0 ]; then
69+
echo "All hook regression tests passed."
70+
else
71+
echo "$fails test(s) failed."
72+
exit 1
73+
fi

0 commit comments

Comments
 (0)