diff --git a/.mcp.json b/.mcp.json index 1ab22fe..cb0e270 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,24 +4,19 @@ "command": "toolbox", "args": ["--config", "${CLAUDE_PLUGIN_ROOT}/tools.yaml", "--stdio"], "env": { - "COCKROACHDB_HOST": "${COCKROACHDB_HOST}", - "COCKROACHDB_PORT": "${COCKROACHDB_PORT}", - "COCKROACHDB_USER": "${COCKROACHDB_USER}", - "COCKROACHDB_PASSWORD": "${COCKROACHDB_PASSWORD}", - "COCKROACHDB_DATABASE": "${COCKROACHDB_DATABASE}", - "COCKROACHDB_SSLMODE": "${COCKROACHDB_SSLMODE}" + "COCKROACHDB_HOST": "${COCKROACHDB_HOST:-localhost}", + "COCKROACHDB_PORT": "${COCKROACHDB_PORT:-26257}", + "COCKROACHDB_USER": "${COCKROACHDB_USER:-root}", + "COCKROACHDB_PASSWORD": "${COCKROACHDB_PASSWORD:-}", + "COCKROACHDB_DATABASE": "${COCKROACHDB_DATABASE:-defaultdb}", + "COCKROACHDB_SSLMODE": "${COCKROACHDB_SSLMODE:-require}" } }, - "cockroachdb-toolbox-http": { - "type": "http", - "url": "http://127.0.0.1:5000/mcp", - "_comment": "SSE transport — run 'toolbox --config tools.yaml' first, then connect via this URL" - }, "cockroachdb-cloud": { "type": "http", "url": "https://cockroachlabs.cloud/mcp", "headers": { - "mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID}" + "mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID:-}" } } } diff --git a/AGENTS.md b/AGENTS.md index 61c78b6..16cc893 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,9 @@ The CockroachDB plugin for Claude Code: MCP backends (`.mcp.json` + `tools.yaml` - **Never edit `skills/` by hand.** Skills are synced from the [cockroachdb-skills](https://github.com/cockroachlabs/cockroachdb-skills) submodule by a weekly workflow that also regenerates the `skills` array in `.claude-plugin/plugin.json`. Changes there get overwritten. Contribute skills upstream instead. - **Never bump versions by hand.** Release Please owns `version` in `.claude-plugin/plugin.json`, `.release-please-manifest.json`, and `CHANGELOG.md`. Use conventional commits: `fix:`/`feat:` cut a release, `chore:`/`docs:` do not (and that is fine, because installs pull from git, not release tarballs). -- **Keep the hook command pattern.** Hook commands load their script through a `runpy` bootstrap with a `\\?\` long-path prefix on Windows and a trailing `; exit 0`. This is load-bearing: it survives plugin cache paths past Windows MAX_PATH (issue #20) and hosts that fail to substitute `${CLAUDE_PLUGIN_ROOT}` (issue #23). A hook must never surface an error on every edit; when the interpreter cannot even open the script, it must fail open. The exact pattern is documented in CONTRIBUTING. +- **Keep the hook command pattern.** Hook commands load their script through a `runpy` bootstrap with a `\\?\` long-path prefix on Windows and a trailing `; exit 0`. This is load-bearing: it survives plugin cache paths past Windows MAX_PATH (issue #20), hosts that fail to substitute `${CLAUDE_PLUGIN_ROOT}` (issue #23), and current Claude Code, which exports `CLAUDE_PLUGIN_ROOT` instead of substituting it in shell-form commands, so the bootstrap falls back to the environment variable. A hook must never surface an error on every edit; when the interpreter cannot even open the script, it must fail open. The exact pattern is documented in CONTRIBUTING. +- **Use `${VAR:-default}` in `.mcp.json`, never a bare `${VAR}`.** An unset bare reference reaches the server as literal text (issue #27). Toolbox uses an empty value as-is instead of its default, so connection settings need non-empty defaults that match `tools.yaml`. +- **Match plugin MCP tools by their scoped name.** Claude Code names them `mcp__plugin_cockroachdb___`, and a hook matcher that lists only the bare `mcp____` name never fires for marketplace installs. - **Hook scripts are Python 3 stdlib only**, read JSON on stdin, write JSON on stdout, and always exit 0 (blocking is signaled via `hookSpecificOutput.permissionDecision`, not exit codes). - **No counts in descriptions.** Do not write "three agents" or "33 skills" anywhere; counts go stale. Name the things instead. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75cdc7a..0e899f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ claude plugin validate . .claude-plugin/ plugin.json # Plugin manifest (version managed by Release Please) marketplace.json # Marketplace catalog entry -.mcp.json # MCP server definitions (stdio, HTTP, Cloud) +.mcp.json # MCP server definitions (Toolbox stdio, Cloud) tools.yaml # MCP Toolbox source and tool definitions agents/ # Agent markdown files (auto-discovered) hooks/ @@ -139,16 +139,19 @@ 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. -- 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: +- Always exit 0. Signal a block with `hookSpecificOutput.permissionDecision: "deny"`, not with an exit code. +- Tools from the plugin's own MCP servers are named `mcp__plugin_cockroachdb___`. A matcher that names only the bare `mcp____` form never fires for people who installed the plugin, so list the scoped name, and keep the bare name too when the hook should also cover a standalone server. +- Load hook scripts through the bootstrap below instead of passing the script path straight to `python3`. Claude Code doesn't substitute `${CLAUDE_PLUGIN_ROOT}` inside a shell-form command; it exports `CLAUDE_PLUGIN_ROOT` and leaves the placeholder to the shell, which never expands it inside single quotes. The bootstrap uses the substituted path when a host provides one and otherwise reads the exported variable. On Windows the plugin root can exceed the 260-character `MAX_PATH` limit (issue #20), so the bootstrap prefixes the `\\?\` long-path escape, built with `chr(92)` so no escaping layer can change it. It exits quietly when it can't find the script (issue #23), and `; exit 0` keeps a failed bootstrap from disrupting editing: ```json - "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" + "command": "python3 -c 'import os, runpy; r = r\"${CLAUDE_PLUGIN_ROOT}\"; r = r if r and not r.startswith(\"$\") else os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\"); p = os.path.normpath(os.path.join(r, \"scripts\", \"your-script.py\")) if r else \"\"; p = chr(92) * 2 + \"?\" + chr(92) + p if p and os.name == \"nt\" else p; p and os.path.isfile(p) and runpy.run_path(p, run_name=\"__main__\")'; exit 0" ``` ### MCP Configuration -- `.mcp.json` defines MCP server backends. -- Use `${ENV_VAR}` syntax for environment variable references. +- `.mcp.json` defines the MCP servers the plugin ships. +- Reference environment variables as `${VAR:-default}`, never as a bare `${VAR}`. Claude Code passes an unset bare reference through as the literal text `${VAR}`, which breaks the server (issue #27). +- Toolbox uses a set-but-empty variable as an empty value rather than falling back to its default, so give each Toolbox connection setting a non-empty default that matches `tools.yaml`. Only `COCKROACHDB_PASSWORD` defaults to empty. +- An optional header uses an empty default, as in `${COCKROACHDB_CLUSTER_ID:-}`. The Cloud MCP server treats an empty `mcp-cluster-id` header as absent. - The `tools.yaml` file uses Toolbox v1.1.0 map-based format with `${VAR:default}` syntax for defaults. ### Skills diff --git a/README.md b/README.md index 4d819a5..95e23d8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,14 @@ Connect [Claude Code](https://code.claude.com/) directly to your CockroachDB clu Install from the [Claude Marketplace](https://claude.com/plugins/cockroachdb), or in Claude Code run: ``` -/install-plugin cockroachdb +/plugin install cockroachdb@claude-plugins-official +``` + +To install from this repository's own marketplace instead: + +``` +/plugin marketplace add cockroachdb/claude-plugin +/plugin install cockroachdb@cockroachdb-claude-plugin ``` ### Local development @@ -21,15 +28,44 @@ claude --plugin-dir /path/to/claude-plugin ### Prerequisites -This plugin connects to CockroachDB via MCP (Model Context Protocol) using [MCP Toolbox for Databases](https://github.com/googleapis/mcp-toolbox) (v1.0.0+): +The skills, agents, and hooks work without any setup. Each of the plugin's MCP servers needs its own: + +- **`cockroachdb-toolbox`** (any self-hosted or Cloud cluster) needs [MCP Toolbox for Databases](https://github.com/googleapis/mcp-toolbox) v1.0.0 or later on your `PATH`, and a running CockroachDB cluster it can reach. Toolbox connects when the session starts, so without a reachable cluster this server fails to start. If you don't run a cluster, [turn the server off](#turn-off-a-server-you-dont-use). +- **`cockroachdb-cloud`** (CockroachDB Cloud) needs a CockroachDB Cloud account. Run `/mcp` and select the server to sign in. + +#### Install MCP Toolbox + +macOS or Linux: ```bash brew install mcp-toolbox ``` +Windows: download `toolbox.exe` into a folder on your `PATH`. In PowerShell, set the version to the latest one on the [Toolbox releases page](https://github.com/googleapis/mcp-toolbox/releases), and use `windows/arm64` in the URL on Arm devices: + +```powershell +$VERSION = "1.14.0" +curl.exe -o toolbox.exe "https://storage.googleapis.com/mcp-toolbox-for-databases/v$VERSION/windows/amd64/toolbox.exe" +``` + +Restart Claude after changing your `PATH`, then confirm the install with `toolbox --version`. For other platforms, the container image, and building from source, see [Install Toolbox](https://github.com/googleapis/mcp-toolbox#install-toolbox). + ## Configuration -Set environment variables for your CockroachDB connection: +### Self-hosted clusters (MCP Toolbox) + +The `cockroachdb-toolbox` server reads its connection settings from environment variables. Each one is optional, and an unset variable falls back to its default: + +| Variable | Default | Notes | +|------------------------|-------------|------------------------------------------------------------------------------| +| `COCKROACHDB_HOST` | `localhost` | | +| `COCKROACHDB_PORT` | `26257` | | +| `COCKROACHDB_USER` | `root` | | +| `COCKROACHDB_PASSWORD` | (empty) | | +| `COCKROACHDB_DATABASE` | `defaultdb` | | +| `COCKROACHDB_SSLMODE` | `require` | Use `disable` for a local `--insecure` cluster, `verify-full` for production | + +Set them in the environment Claude starts from. On macOS or Linux, add them to your shell profile: ```bash export COCKROACHDB_HOST="your-cluster-host" @@ -40,11 +76,25 @@ export COCKROACHDB_DATABASE="your-database" export COCKROACHDB_SSLMODE="verify-full" ``` -For CockroachDB Cloud, find connection details in the [Cloud Console](https://cockroachlabs.cloud/). +On Windows, set them as user environment variables, for example `setx COCKROACHDB_HOST "your-cluster-host"`, then restart Claude. + +For a CockroachDB Cloud cluster, find the connection details in the [Cloud Console](https://cockroachlabs.cloud/). + +Toolbox runs in read-only mode: `SELECT`, `SHOW`, and `EXPLAIN` work, and writes and schema changes are rejected. To allow writes, run Toolbox with your own copy of [`tools.yaml`](tools.yaml) that sets `enableWriteMode: true`, configured as your own MCP server, and turn off the plugin's `cockroachdb-toolbox` server. + +### CockroachDB Cloud + +The `cockroachdb-cloud` server connects to the [managed MCP server](https://www.cockroachlabs.com/docs/cockroachcloud/connect-to-the-cockroachdb-cloud-mcp-server) that Cockroach Labs hosts. Run `/mcp`, select `cockroachdb-cloud`, and sign in. The consent screen asks you to grant read access, write access, or both. + +By default the connection can reach every cluster your CockroachDB Cloud role allows. To limit it to one cluster, set `COCKROACHDB_CLUSTER_ID` to that cluster's ID. + +### Turn off a server you don't use + +Claude Code starts both MCP servers in every session. If you only use one of them, open `/mcp`, select the other, and disable it. Claude Code remembers the choice for each project. ### Alternative MCP Backends -The plugin ships with the **MCP Toolbox** (stdio) backend active by default. To use a different backend, replace the contents of `.mcp.json`: +The plugin ships with the **MCP Toolbox** (stdio) and **CockroachDB Cloud MCP** backends active by default. To add another backend, put its entry in your own MCP configuration (for example a project `.mcp.json`) or add it with `claude mcp add`:
MCP Toolbox via HTTP (remote/multi-user) @@ -60,6 +110,8 @@ The plugin ships with the **MCP Toolbox** (stdio) backend active by default. To } ``` +Or via CLI: `claude mcp add --transport http cockroachdb-toolbox-http http://your-toolbox-host:5000/mcp` + Run Toolbox in HTTP mode: `toolbox --config tools.yaml --address 0.0.0.0 --port 5000` Run Toolbox with the built-in web UI: `toolbox --config tools.yaml --ui --port 5000` (opens at `http://127.0.0.1:5000/ui`) @@ -67,6 +119,41 @@ Run Toolbox with the built-in web UI: `toolbox --config tools.yaml --ui --port 5 > **Note:** Toolbox must successfully connect to CockroachDB on startup. If the database is unreachable (wrong host/port, env vars not set), the server will hang during initialization and the UI will be stuck on "Fetching tools...". Make sure your `COCKROACHDB_*` environment variables are set and the database is accessible before starting.
+
+CockroachDB MCP Server (first-party, self-hosted) + +[CockroachDB MCP Server](https://github.com/cockroachdb/cockroachdb-mcp-server) is Cockroach Labs' own MCP server for clusters you run yourself. By default it registers only read-only tools, such as `list_databases`, `list_tables`, `get_table_schema`, `select_query`, `explain_query`, `show_statement`, and `show_running_queries`. Setting `CRDB_MCP_ENABLE_WRITE_QUERIES=true` adds `create_database`, `create_table`, `insert_rows`, `update_rows`, and `delete_rows`, and the server refuses an `UPDATE` or `DELETE` without a `WHERE` clause. + +**Install:** `go install github.com/cockroachdb/cockroachdb-mcp-server@latest` (Go 1.26+). Linux and Windows binaries and a Docker image are listed on the [releases page](https://github.com/cockroachdb/cockroachdb-mcp-server/releases). There are no prebuilt macOS binaries, so on macOS use `go install` or Docker. + +**Configure** with certificate authentication (recommended): +```json +{ + "mcpServers": { + "cockroachdb-mcp-server": { + "command": "cockroachdb-mcp-server", + "env": { + "CRDB_HOST": "your-cluster-host", + "CRDB_USERNAME": "ai_agent", + "CRDB_SSL_MODE": "verify-full", + "CRDB_SSL_CA_PATH": "/certs/ca.crt", + "CRDB_SSL_CERTFILE": "/certs/client.ai_agent.crt", + "CRDB_SSL_KEYFILE": "/certs/client.ai_agent.key" + } + } + } +} +``` + +For a local `--insecure` development cluster: + +```bash +claude mcp add cockroachdb-mcp-server -e CRDB_DATABASE_URL="postgresql://root@localhost:26257/defaultdb?sslmode=disable" -e CRDB_MCP_ALLOW_INSECURE_DB=true -- cockroachdb-mcp-server +``` + +Password authentication is off unless you set `CRDB_MCP_ALLOW_PASSWORD_AUTH=true`. Desktop apps may not search `~/go/bin`, so give the binary's absolute path as the `command` if the server doesn't start. The plugin's SQL safety hook applies to the bundled Toolbox server; this server enforces its own guardrails. See the [server's README](https://github.com/cockroachdb/cockroachdb-mcp-server#configuration) for every setting. +
+
ccloud CLI (cluster lifecycle, backups, DR, networking) @@ -120,24 +207,21 @@ ccloud cluster disruption set my-cluster --region us-east-1 --whole-region
CockroachDB Cloud MCP Server (OAuth/API key) -The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) is hosted by Cockroach Labs and requires no infrastructure setup. Authenticate via OAuth 2.1 (PKCE) or a service account API key. Read-only by default; write access requires explicit consent. +The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) is hosted by Cockroach Labs and requires no infrastructure setup. The plugin already includes it as `cockroachdb-cloud`; use these snippets to configure it yourself, for example with a service account API key. With OAuth 2.1 (PKCE), the consent screen asks you to grant read access, write access, or both (scopes `mcp:read` and `mcp:write`). -**OAuth (recommended — opens browser for consent, scopes: `mcp:read`, `mcp:write`):** +**OAuth (recommended, opens a browser for consent):** ```json { "mcpServers": { "cockroachdb-cloud": { "type": "http", - "url": "https://cockroachlabs.cloud/mcp", - "headers": { - "mcp-cluster-id": "{your-cluster-id}" - } + "url": "https://cockroachlabs.cloud/mcp" } } } ``` -**API Key (headless/autonomous agents):** +**API key (headless or autonomous agents):** ```json { "mcpServers": { @@ -145,7 +229,6 @@ The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb "type": "http", "url": "https://cockroachlabs.cloud/mcp", "headers": { - "mcp-cluster-id": "{your-cluster-id}", "Authorization": "Bearer {your-service-account-api-key}" } } @@ -153,7 +236,9 @@ The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb } ``` -Or via CLI: `claude mcp add cockroachdb-cloud https://cockroachlabs.cloud/mcp --transport http --header "mcp-cluster-id: {your-cluster-id}"` +Without an `mcp-cluster-id` header, a connection can reach every cluster your user or service account can access. To limit it to one cluster, add `"mcp-cluster-id": "{your-cluster-id}"` to `headers`. + +Or via CLI: `claude mcp add --transport http cockroachdb-cloud https://cockroachlabs.cloud/mcp`, adding `--header "mcp-cluster-id: {your-cluster-id}"` to limit the connection to one cluster. See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/connect-to-the-cockroachdb-cloud-mcp-server) for detailed setup.
@@ -166,7 +251,8 @@ See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/con |----------------------------|-------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------| | `cockroachdb-toolbox` | Active | stdio | Any CockroachDB cluster via [MCP Toolbox](https://github.com/googleapis/mcp-toolbox) | | `cockroachdb-cloud` | Active | Streamable HTTP | [Managed MCP Server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) — CockroachDB Cloud (OAuth/API key) | -| `cockroachdb-toolbox-http` | Available | SSE | MCP Toolbox remote/multi-user via HTTP | +| `cockroachdb-toolbox-http` | Available | HTTP | MCP Toolbox remote/multi-user via HTTP (not shipped; add it yourself, see Alternative MCP Backends) | +| `cockroachdb-mcp-server` | Available | stdio, HTTPS | First-party server for self-hosted clusters (not shipped; add it yourself, see Alternative MCP Backends) | ### CLI Tools @@ -180,7 +266,7 @@ See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/con | Tool | Description | |----------------------------|--------------------------------------------------| -| `cockroachdb-execute-sql` | Execute SQL statements (SELECT, DDL, DML) | +| `cockroachdb-execute-sql` | Run SQL (read-only unless write mode is enabled) | | `cockroachdb-list-schemas` | List all schemas in the database | | `cockroachdb-list-tables` | List tables with columns, types, and constraints | diff --git a/agents/cockroachdb-dba.md b/agents/cockroachdb-dba.md index cd4c7fe..819b556 100644 --- a/agents/cockroachdb-dba.md +++ b/agents/cockroachdb-dba.md @@ -33,7 +33,7 @@ You are a CockroachDB database administration expert. You specialize in: ## Available MCP Tools **Via MCP Toolbox** (self-hosted, any cluster): -- `cockroachdb-execute-sql`: Execute any SQL statement +- `cockroachdb-execute-sql`: Run SQL (read-only by default: SELECT, SHOW, EXPLAIN; writes and DDL need write mode enabled in the Toolbox config) - `cockroachdb-list-schemas`: List database schemas - `cockroachdb-list-tables`: List tables with column details diff --git a/agents/cockroachdb-developer.md b/agents/cockroachdb-developer.md index 6ff2df9..d64dddb 100644 --- a/agents/cockroachdb-developer.md +++ b/agents/cockroachdb-developer.md @@ -179,7 +179,7 @@ When bulk DML exceeds 250K-500K rows (or 1M+ without secondary indexes): ## Available MCP Tools **Via MCP Toolbox** (self-hosted, any cluster): -- `cockroachdb-execute-sql`: Execute any SQL statement +- `cockroachdb-execute-sql`: Run SQL (read-only by default: SELECT, SHOW, EXPLAIN; writes and DDL need write mode enabled in the Toolbox config) - `cockroachdb-list-schemas`: List database schemas - `cockroachdb-list-tables`: List tables with column details diff --git a/agents/cockroachdb-operator.md b/agents/cockroachdb-operator.md index 707bc02..af8ef6e 100644 --- a/agents/cockroachdb-operator.md +++ b/agents/cockroachdb-operator.md @@ -262,7 +262,7 @@ For batch DML exceeding 250K-500K rows: ## Available MCP Tools **Via MCP Toolbox** (self-hosted, any cluster): -- `cockroachdb-execute-sql`: Execute any SQL statement (diagnostics, SHOW commands, DDL) +- `cockroachdb-execute-sql`: Run SQL for diagnostics and SHOW commands (read-only by default; DDL and other writes need write mode enabled in the Toolbox config) - `cockroachdb-list-schemas`: List database schemas - `cockroachdb-list-tables`: List tables with column details diff --git a/hooks/hooks.json b/hooks/hooks.json index 95be03b..36686eb 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -2,11 +2,11 @@ "hooks": { "PreToolUse": [ { - "matcher": "mcp__cockroachdb-toolbox__cockroachdb-execute-sql", + "matcher": "mcp__plugin_cockroachdb_cockroachdb-toolbox__cockroachdb-execute-sql|mcp__cockroachdb-toolbox__cockroachdb-execute-sql", "hooks": [ { "type": "command", - "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", + "command": "python3 -c 'import os, runpy; r = r\"${CLAUDE_PLUGIN_ROOT}\"; r = r if r and not r.startswith(\"$\") else os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\"); p = os.path.normpath(os.path.join(r, \"scripts\", \"validate-sql.py\")) if r else \"\"; p = chr(92) * 2 + \"?\" + chr(92) + p if p and os.name == \"nt\" else p; p and os.path.isfile(p) and runpy.run_path(p, run_name=\"__main__\")'; exit 0", "timeout": 10 } ] @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "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", + "command": "python3 -c 'import os, runpy; r = r\"${CLAUDE_PLUGIN_ROOT}\"; r = r if r and not r.startswith(\"$\") else os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\"); p = os.path.normpath(os.path.join(r, \"scripts\", \"check-sql-files.py\")) if r else \"\"; p = chr(92) * 2 + \"?\" + chr(92) + p if p and os.name == \"nt\" else p; p and os.path.isfile(p) and runpy.run_path(p, run_name=\"__main__\")'; exit 0", "timeout": 15 } ] diff --git a/scripts/test-hooks.sh b/scripts/test-hooks.sh index a714b98..a4769cc 100755 --- a/scripts/test-hooks.sh +++ b/scripts/test-hooks.sh @@ -27,11 +27,17 @@ hook_cmd() { # $1=event $2=root } # check: desc, event, root, stdin, expected_rc, mode(empty|contains), substr +# The command runs with CLAUDE_PLUGIN_ROOT unset unless ENV_ROOT is set, in +# which case it is exported the way Claude Code exports it to hook processes. 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=$? + if [ -n "${ENV_ROOT:-}" ]; then + out="$(printf '%s' "$stdin" | CLAUDE_PLUGIN_ROOT="$ENV_ROOT" sh -c "$cmd" 2>/dev/null)"; rc=$? + else + out="$(printf '%s' "$stdin" | env -u CLAUDE_PLUGIN_ROOT sh -c "$cmd" 2>/dev/null)"; rc=$? + fi [ "$rc" = "$want_rc" ] || ok=0 case "$mode" in empty) [ -z "$out" ] || ok=0 ;; @@ -64,6 +70,18 @@ check "non-SQL edit produces no block" PostToolUse "$ROOT" "{\"tool_input\":{\" 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 +# --- regression: placeholder left for the shell, root exported (current Claude Code) --- +# Claude Code no longer substitutes ${CLAUDE_PLUGIN_ROOT} inside a shell-form +# command. It exports CLAUDE_PLUGIN_ROOT and lets the shell expand the +# placeholder, which never happens inside the single-quoted bootstrap. In a +# PowerShell shell it rewrites the placeholder to ${env:CLAUDE_PLUGIN_ROOT}, +# which single quotes also leave unexpanded. The bootstrap must read the +# exported variable in both cases. +ENV_ROOT="$ROOT" check "PreToolUse uses the exported root when the placeholder is not substituted" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 contains '"permissionDecision": "deny"' +ENV_ROOT="$ROOT" check "PostToolUse uses the exported root when the placeholder is not substituted" PostToolUse '${CLAUDE_PLUGIN_ROOT}' "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 contains 'CockroachDB lint' +ENV_ROOT="$ROOT" check "PreToolUse uses the exported root when the placeholder is in PowerShell form" PreToolUse '${env:CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 contains '"permissionDecision": "deny"' +ENV_ROOT="$TMP/missing" check "PreToolUse fails open when the exported root has no script" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 empty + echo if [ "$fails" -eq 0 ]; then echo "All hook regression tests passed." diff --git a/tools.yaml b/tools.yaml index 453c048..1802d2f 100644 --- a/tools.yaml +++ b/tools.yaml @@ -21,9 +21,11 @@ tools: type: cockroachdb-execute-sql source: crdb-source description: | - Execute ad-hoc SQL statements against a CockroachDB database. - Use this for any SQL: SELECT queries, DDL statements (CREATE, ALTER, DROP), - and DML statements (INSERT, UPDATE, DELETE). + Execute an ad-hoc SQL statement against a CockroachDB database. + The connection is read-only by default: SELECT, SHOW, and EXPLAIN work, + while writes and schema changes (INSERT, UPDATE, DELETE, CREATE, ALTER, + DROP) are rejected unless write mode is enabled in the Toolbox + configuration (enableWriteMode: true). cockroachdb-list-schemas: type: cockroachdb-list-schemas