Skip to content

feat(databricks-devtools): add Databricks CLI wrapper plugin - #408

Merged
baleen37 merged 1 commit into
mainfrom
feat/databricks
Feb 5, 2026
Merged

feat(databricks-devtools): add Databricks CLI wrapper plugin#408
baleen37 merged 1 commit into
mainfrom
feat/databricks

Conversation

@baleen37

@baleen37 baleen37 commented Feb 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add databricks-devtools plugin for Claude Code
  • Wraps Databricks CLI for workspace management and SQL execution
  • Implements CLI runner with output parsing (JSON, table, psql formats)
  • Parses ~/.databrickscfg configuration file
  • MCP tools: list_profiles, get_profile_info
  • Commands: /databricks, /databricks:sql
  • 30 tests covering all core functionality

Technical Details

  • Location: plugins/databricks-devtools/
  • Dependencies: @modelcontextprotocol/sdk, zod
  • Built with: esbuild, TypeScript
  • Test Framework: vitest

Files Changed

  • 26 files added, 5523 insertions
  • Core modules: cli/runner.ts, cli/parser.ts, config/databrickscfg.ts
  • MCP server with 2 tools
  • 2 commands and 1 skill for documentation

Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added Databricks DevTools Plugin enabling workspace management and SQL execution.
    • Multi-profile support with automatic Git branch to Databricks profile mapping for environment selection.
    • SQL warehouse discovery, session management, and query execution capabilities.
    • MCP (Model Context Protocol) integration for Claude AI support.
    • Comprehensive documentation including workflow examples, configuration guidance, and troubleshooting.

@baleen37
baleen37 enabled auto-merge (squash) February 5, 2026 04:00
@coderabbitai

coderabbitai Bot commented Feb 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@baleen37 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 41 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This pull request introduces a complete new Databricks DevTools plugin with manifest configuration, build infrastructure, TypeScript source implementation for CLI integration and MCP server functionality, comprehensive documentation, and test coverage across all modules.

Changes

Cohort / File(s) Summary
Plugin Configuration & Manifest
plugins/databricks-devtools/.claude-plugin/plugin.json, plugins/databricks-devtools/.mcp.json, plugins/databricks-devtools/package.json
Plugin metadata, MCP server configuration, and npm package manifest defining the databricks-devtools plugin v1.0.0 with dependencies on @modelcontextprotocol/sdk and zod.
Build & Runtime Infrastructure
plugins/databricks-devtools/scripts/build.mjs, plugins/databricks-devtools/scripts/mcp-server-wrapper.mjs, plugins/databricks-devtools/tsconfig.json, plugins/databricks-devtools/vitest.config.ts
Build orchestration, cross-platform wrapper handling dependency resolution and compilation, TypeScript strict mode configuration, and Vitest test runner setup.
Documentation
plugins/databricks-devtools/README.md, plugins/databricks-devtools/commands/databricks.md, plugins/databricks-devtools/commands/databricks.sql.md, plugins/databricks-devtools/skills/using-databricks-devtools/SKILL.md
Comprehensive plugin guide including installation, MCP tools reference, workspace and SQL command workflows, configuration examples, and troubleshooting guidance.
CLI Module
plugins/databricks-devtools/src/cli/index.ts, plugins/databricks-devtools/src/cli/runner.ts, plugins/databricks-devtools/src/cli/parser.ts, plugins/databricks-devtools/tests/cli/*.test.ts
CLI command execution wrapper with subprocess management, output parsing for JSON/table/psql formats, profile injection, and unit tests validating parser and runner behavior.
Config Module
plugins/databricks-devtools/src/config/index.ts, plugins/databricks-devtools/src/config/databrickscfg.ts, plugins/databricks-devtools/src/config/profiles.ts, plugins/databricks-devtools/src/config/types.ts, plugins/databricks-devtools/tests/config/*.test.ts, plugins/databricks-devtools/tests/databrickscfg.test.ts, plugins/databricks-devtools/tests/profiles.test.ts
INI-style config file parsing, default config path resolution, TypeScript types for profiles and config data, and comprehensive test coverage for config scenarios.
MCP Server Module
plugins/databricks-devtools/src/mcp/server.ts, plugins/databricks-devtools/tests/mcp/server.test.ts
MCP server with tools for listing and validating Databricks profiles, fetching profile info, and stdio transport with error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #373: Implements matching cross-platform MCP server wrapper pattern with build step integration to copy wrapper to dist directory
  • PR #30: Adds dynamic plugin discovery via release configuration that would detect new plugin manifests like the .claude-plugin/plugin.json introduced here

Suggested labels

released

Poem

🐰 A new plugin hops into the warren,
Databricks profiles in order and true,
CLI commands and config so stellar,
SQL queries with wrappers brand new,
The tests all pass, the code structure's bright—
DevTools for Claude, shining with light! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the primary change: adding a new Databricks CLI wrapper plugin. It is concise, specific, and directly related to the main scope of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/databricks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@plugins/databricks-devtools/commands/databricks.sql.md`:
- Around line 235-238: The bold emphasis on the phrase currently has spaces
inside the emphasis markers (e.g., "**"Query timeout"**"); remove the spaces
between the asterisks and the quoted text so the emphasis markers directly wrap
the text (e.g., change to **"Query timeout"**) to satisfy MD037; update any
other occurrences in the same block that have spaces inside `*`/`**` markers.
- Around line 199-203: Replace the sample secret token in the INI example under
the [dev] section by redacting the real-looking value; update the token line
(the "token" key in the example block) to use a clear placeholder like
"YOUR_TOKEN_HERE" or "<REDACTED_TOKEN>" so the example no longer contains a real
secret and won't trigger Gitleaks.

In `@plugins/databricks-devtools/package.json`:
- Around line 12-21: The esbuild dependency needs to be updated to a patched
minimum version: change the "esbuild" entry in package.json from "^0.20.0" to at
least "^0.25.0" (recommend "^0.25.0" or later), keep or optionally bump "vitest"
(currently "^2.0.0") if you want newer features but it's not required for the
immediate security fix, and optionally consider updating "typescript" and
"@types/node" later; after updating the "esbuild" version run your package
manager (npm/yarn/pnpm) to regenerate lockfiles and verify the dev server starts
and no vulnerable esbuild version is installed.

In `@plugins/databricks-devtools/skills/using-databricks-devtools/SKILL.md`:
- Around line 60-70: Replace the realistic-looking `token` values in SKILL.md
with unmistakable placeholders or env-var references (e.g., TOKEN_PLACEHOLDER or
${DATABRICKS_TOKEN}) so the `[dev]` and `[prod]` example blocks no longer
contain plausible API tokens; update the `token` fields in those example ini
blocks and, if desired, add a short comment noting they should be replaced with
real tokens stored in environment variables.

In `@plugins/databricks-devtools/src/cli/parser.ts`:
- Around line 20-41: The separator detection is too permissive and treats data
rows containing hyphens (e.g., dates) as the table separator; update the checks
around separatorIndex discovery and the loop break condition (the findIndex
call, and the in-loop condition that currently uses line.includes('|') &&
/[-]+/.test(line) && !line.includes('a')) to use a stricter "table divider"
regex that only matches lines composed of pipes, dashes, colons and whitespace
(no letters/numbers), e.g., replace that boolean check with a test like "line
matches a regex that allows only |, -, :, and whitespace" so headerLine,
headers, parseTableWithoutSeparator and the loop over lines correctly treat real
separator rows and do not truncate result rows.

In `@plugins/databricks-devtools/src/cli/runner.ts`:
- Around line 1-43: The PATH split in findDatabricksExecutable uses a hardcoded
':' which fails on Windows; update that logic to use Node's platform-specific
delimiter by importing path (e.g., import { delimiter } from 'node:path' or use
path.delimiter) and replace pathEnv.split(':') with pathEnv.split(delimiter) so
path resolution (in findDatabricksExecutable, alongside DEFAULT_DATABRICKS_PATH
and customPath handling) works cross-platform.
- Around line 97-103: The close handler on the child process currently masks
signal termination by treating code === null as exitCode: 0; update the
child.on('close', ...) callback (in runner.ts where the spawned child is
referenced as child and the surrounding promise uses resolve/reject) to accept
the second parameter (signal) and, when code === null, reject the promise with
an Error that includes the signal name and context (instead of resolving with
exitCode 0); only resolve with { stdout, stderr, exitCode: code } for non-null
codes. Ensure you reference the same child and resolve/reject variables so
callers receive a rejection on signal termination.

In `@plugins/databricks-devtools/src/config/databrickscfg.ts`:
- Around line 23-37: The parser currently skips adding the DEFAULT profile by
setting currentProfile to null when profileName === 'DEFAULT'; change this so
DEFAULT is treated like any other profile: when sectionMatch yields profileName
(including 'DEFAULT') set currentProfile = profileName and currentConfig = {},
and ensure that when switching sections you save profiles[currentProfile] =
currentConfig as done for other profiles (symbols: sectionMatch, profileName,
currentProfile, currentConfig, profiles); this will allow list_profiles() and
get_profile_info('DEFAULT') to find the DEFAULT entry.

In `@plugins/databricks-devtools/src/mcp/server.ts`:
- Around line 73-111: getProfileInfoTool currently returns profileConfig
verbatim which can leak secrets (e.g., token); modify getProfileInfoTool so that
before JSON.stringify it sanitizes profileConfig by removing or masking
sensitive keys (e.g., token, password, secret, access_token, secret_key) —
implement a small sanitizer used on the profileConfig (or build a shallow copy)
that replaces values for keys matching a case-insensitive pattern like
/(token|secret|password|access(_)?token|secret_key)/i with a fixed mask such as
"***REDACTED***" (or omit those keys entirely), then return the sanitized object
instead of the raw profileConfig.
- Around line 23-71: The validation currently treats any runCommand call as
success if it doesn't throw; update listProfilesTool to inspect the
CommandResult returned by runCommand(['workspace','list','/'], { profile: name
}) instead of only relying on absence of exceptions: store the result (e.g.,
const result = await runCommand(...)), set valid = result.exitCode === 0, and
when exitCode !== 0 or an exception is thrown capture the stderr/stdout or error
message into the profile entry (or include an error field) so the JSON output
surfaces actionable error details rather than swallowing them; reference
runCommand and listProfilesTool to locate where to change the logic.

In `@plugins/databricks-devtools/tests/cli/runner.test.ts`:
- Around line 9-21: Update the tests to avoid invoking the real databricks CLI
by passing a deterministic mocked path via the runCommand helper (use the
existing databricksPath option when calling runCommand in both tests, e.g.,
provide a stubbed executable path or an env-driven placeholder) so CI doesn't
require the real CLI; also replace the empty-body "empty stdin" test with
it.todo(...) to mark it as pending. Ensure you update the two occurrences where
runCommand(...) is called (including the one with the profile option) and change
the empty stdin test declaration to it.todo.
🧹 Nitpick comments (4)
plugins/databricks-devtools/README.md (1)

104-145: Add language specifiers to fenced code blocks.

The code blocks on lines 106 and 128 are missing language specifiers. Adding bash or text would improve syntax highlighting and satisfy markdown linting rules.

📝 Suggested fix
 **Examples:**

-```
+```bash
 # List workspace root
 /databricks workspace ls /

And similarly for line 128:

 **Examples:**

-```
+```bash
 # Simple query
 /databricks:sql "SELECT * FROM schema.table LIMIT 10"
plugins/databricks-devtools/tests/cli/runner.test.ts (1)

24-27: Mark the stdin test as todo/skip instead of an empty body.

✅ Minimal change
-  it('should pass input via stdin', async () => {
-    // This will fail because we don't have a good test command that uses stdin
-    // For now, we'll skip this and write it when we have a real use case
-  });
+  it.todo('should pass input via stdin');
plugins/databricks-devtools/scripts/build.mjs (1)

31-33: Prefer npx --no-install (or local tsc) for hermetic builds.

♻️ Safer invocation
-    await execAsync("npx tsc --project tsconfig.json");
+    await execAsync("npx --no-install tsc --project tsconfig.json");
plugins/databricks-devtools/scripts/mcp-server-wrapper.mjs (1)

131-158: Re-run npm install when package/lockfile changes.
Line 136 only checks node_modules existence, so upgrades with new deps can skip install and then fail build. Consider comparing package.json/package-lock.json mtimes.

♻️ Suggested adjustment
 async function ensureDependenciesAndBuild() {
   const nodeModulesPath = join(PLUGIN_ROOT, 'node_modules');
+  const packageLockPath = join(PLUGIN_ROOT, 'package-lock.json');
   const mcpServerPath = join(PLUGIN_ROOT, 'dist', 'mcp-server.mjs');
   const packageJsonPath = join(PLUGIN_ROOT, 'package.json');
 
-  // Check if node_modules exists
-  if (!existsSync(nodeModulesPath)) {
-    await runNpmInstall();
-  }
+  const nodeModulesExists = existsSync(nodeModulesPath);
+  const nodeModulesMtime = nodeModulesExists ? statSync(nodeModulesPath).mtimeMs : 0;
+  const packageJsonMtime = existsSync(packageJsonPath) ? statSync(packageJsonPath).mtimeMs : 0;
+  const packageLockMtime = existsSync(packageLockPath) ? statSync(packageLockPath).mtimeMs : 0;
+  const needsInstall =
+    !nodeModulesExists ||
+    packageJsonMtime > nodeModulesMtime ||
+    packageLockMtime > nodeModulesMtime;
+
+  if (needsInstall) {
+    await runNpmInstall();
+  }

Comment on lines +199 to +203
```ini
[dev]
host = https://dev.cloud.databricks.com
token = dapi123456789
warehouse_id = 1111-2222-3333

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact the example token value.

The sample token looks like a real secret and was flagged by Gitleaks. Please replace with a clear placeholder.

🔒 Suggested redaction
 [dev]
 host = https://dev.cloud.databricks.com
-token = dapi123456789
+token = ${DATABRICKS_TOKEN}
 warehouse_id = 1111-2222-3333
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```ini
[dev]
host = https://dev.cloud.databricks.com
token = dapi123456789
warehouse_id = 1111-2222-3333
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 202-202: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/commands/databricks.sql.md` around lines 199 -
203, Replace the sample secret token in the INI example under the [dev] section
by redacting the real-looking value; update the token line (the "token" key in
the example block) to use a clear placeholder like "YOUR_TOKEN_HERE" or
"<REDACTED_TOKEN>" so the example no longer contains a real secret and won't
trigger Gitleaks.

Comment on lines +235 to +238
**"Query timeout"**
- Large queries may take longer
- Consider increasing warehouse size
- Add filters or LIMIT to reduce data volume

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix MD037: remove spaces inside emphasis markers.

markdownlint reports spaces inside emphasis markers at Line 238. Please remove the extra spaces inside the */** markers in that line to satisfy MD037.

🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 238-238: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)

🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/commands/databricks.sql.md` around lines 235 -
238, The bold emphasis on the phrase currently has spaces inside the emphasis
markers (e.g., "**"Query timeout"**"); remove the spaces between the asterisks
and the quoted text so the emphasis markers directly wrap the text (e.g., change
to **"Query timeout"**) to satisfy MD037; update any other occurrences in the
same block that have spaces inside `*`/`**` markers.

Comment on lines +12 to +21
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.0.0",
"esbuild": "^0.20.0",
"typescript": "^5.3.3",
"vitest": "^2.0.0"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for latest versions of the specified dependencies

echo "=== Checking npm registry for latest versions ==="
npm_packages=(
  "@modelcontextprotocol/sdk"
  "zod"
  "esbuild"
  "typescript"
  "vitest"
  "@types/node"
)

for pkg in "${npm_packages[@]}"; do
  echo "Package: $pkg"
  npm view "$pkg" version 2>&1 || echo "  (error fetching version)"
  echo ""
done

Repository: baleen37/claude-plugins

Length of output: 284


🌐 Web query:

security vulnerabilities zod typescript esbuild vitest 2025 2026

💡 Result:

Notable vulns affecting these packages (2025–Feb 5, 2026)

Vitest

  • CVE-2025-24964 (Critical RCE): If Vitest’s API server is enabled (e.g., via Vitest UI), it was vulnerable to cross-site WebSocket hijacking, allowing an attacker to modify a test file and trigger execution (RCE). Fixed in 1.6.1 / 2.1.9 / 3.0.5. [1]

esbuild

  • GHSA-67mh-4wv8-2f99 (Moderate): esbuild’s dev server “serve” feature used permissive default CORS (Access-Control-Allow-Origin: *), allowing any website to send requests to the dev server and read responses (potential source/code disclosure, incl. via sourcemaps). Patched in 0.25.0. [2]

Zod

  • I did not find widely reported 2025–2026 CVEs specifically for Zod; the main commonly referenced issue is older:
    • CVE-2023-4316 (DoS / ReDoS in email validation) affecting 3.21.0–3.22.3. (Not a 2025/26 disclosure, but still relevant if you’re pinned.) [3]

TypeScript (the typescript compiler package)

  • I did not find an NVD/GitHub-advisory CVE specifically for the TypeScript compiler (typescript on npm) in 2025–2026 in the sources queried. (Many 2025–2026 TypeScript-related CVEs are in TypeScript-based tools/frameworks, not the compiler itself.)

Practical takeaways

  • If you use Vitest UI / API server: upgrade to ≥ 3.0.5 (or the patched line you’re on). [1]
  • If you use esbuild serve (directly or via tooling): upgrade esbuild to ≥ 0.25.0. [2]
  • If you use Zod email validation and are on 3.21.0–3.22.3: upgrade past the affected range. [3]

Sources: [1] NVD CVE-2025-24964, [2] GitHub Advisory GHSA-67mh-4wv8-2f99, [3] NVD CVE-2023-4316.


🌐 Web query:

@modelcontextprotocol/sdk latest version 2026

💡 Result:

The latest released version of @modelcontextprotocol/sdk I can confirm is 1.25.1 (tagged as Latest on the official GitHub releases page on Dec 16, 2025). [1]

Third-party npm metadata trackers also report 1.25.1 as the latest npm version. [2]

Install:

npm i `@modelcontextprotocol/sdk`@1.25.1

Sources: 1 2

Citations:


Update esbuild to ≥0.25.0 to patch a moderate CORS bypass vulnerability in the dev server.

The dependency versions are outdated and have known security advisories:

  • esbuild ^0.20.0: Vulnerable to GHSA-67mh-4wv8-2f99 (CORS bypass in dev server). The caret constraint will not pull the patched 0.25.0. Update to at least "esbuild": "^0.25.0" or later.
  • vitest ^2.0.0: While outdated (latest is 4.0.18), the caret constraint will pull ≥2.1.9, which includes the fix for CVE-2025-24964 (RCE in API server mode).
  • zod ^3.23.8: Safe from CVE-2023-4316 (DoS in email validation), which affected 3.21.0–3.22.3.
  • @types/node, typescript: Outdated but without known compiler vulnerabilities; consider upgrading for latest features and type definitions.
🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/package.json` around lines 12 - 21, The esbuild
dependency needs to be updated to a patched minimum version: change the
"esbuild" entry in package.json from "^0.20.0" to at least "^0.25.0" (recommend
"^0.25.0" or later), keep or optionally bump "vitest" (currently "^2.0.0") if
you want newer features but it's not required for the immediate security fix,
and optionally consider updating "typescript" and "@types/node" later; after
updating the "esbuild" version run your package manager (npm/yarn/pnpm) to
regenerate lockfiles and verify the dev server starts and no vulnerable esbuild
version is installed.

Comment on lines +60 to +70
```ini
[dev]
host = https://dev.cloud.databricks.com
token = dapi123456789
cluster_id = 1234-567890-abcde
warehouse_id = 1111-2222-3333

[prod]
host = https://prod.cloud.databricks.com
token = dapi987654321
warehouse_id = 4444-5555-6666

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace example tokens with unmistakable placeholders.

Gitleaks flagged the sample token values. Even in docs, realistic-looking tokens can be copied into real configs or accidentally be real. Use an obvious placeholder or env-var reference.

🔒 Suggested redaction
 [dev]
 host = https://dev.cloud.databricks.com
-token = dapi123456789
+token = ${DATABRICKS_TOKEN}
 cluster_id = 1234-567890-abcde
 warehouse_id = 1111-2222-3333

 [prod]
 host = https://prod.cloud.databricks.com
-token = dapi987654321
+token = ${DATABRICKS_TOKEN}
 warehouse_id = 4444-5555-6666
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```ini
[dev]
host = https://dev.cloud.databricks.com
token = dapi123456789
cluster_id = 1234-567890-abcde
warehouse_id = 1111-2222-3333
[prod]
host = https://prod.cloud.databricks.com
token = dapi987654321
warehouse_id = 4444-5555-6666
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 63-63: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 69-69: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/skills/using-databricks-devtools/SKILL.md` around
lines 60 - 70, Replace the realistic-looking `token` values in SKILL.md with
unmistakable placeholders or env-var references (e.g., TOKEN_PLACEHOLDER or
${DATABRICKS_TOKEN}) so the `[dev]` and `[prod]` example blocks no longer
contain plausible API tokens; update the `token` fields in those example ini
blocks and, if desired, add a short comment noting they should be replaced with
real tokens stored in environment variables.

Comment on lines +20 to +41
// Find separator line (contains dashes and pipes)
const separatorIndex = lines.findIndex(line =>
line.includes('|') && /[-]+/.test(line)
);

if (separatorIndex === -1) {
// No separator found, try to use first line as header if it has pipes
return parseTableWithoutSeparator(lines);
}

const headerLine = lines[separatorIndex - 1];
const headers = headerLine.split('|').map(h => h.trim()).filter(h => h);

const result: Array<Record<string, string>> = [];

for (let i = separatorIndex + 1; i < lines.length; i++) {
const line = lines[i];

// Stop if we hit another separator or empty line
if (line.includes('|') && /[-]+/.test(line) && !line.includes('a')) {
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix separator detection to avoid truncating rows with hyphens.

The current separator check (line.includes('|') && /[-]+/.test(line) && !line.includes('a')) will stop on legitimate data rows that contain - (e.g., dates like 2025-02-05) and no a. This can truncate results.

✅ Safer separator detection
-  // Find separator line (contains dashes and pipes)
-  const separatorIndex = lines.findIndex(line =>
-    line.includes('|') && /[-]+/.test(line)
-  );
+  const isSeparatorLine = (line: string) =>
+    /^\s*\|?\s*-{2,}(?:\s*\|\s*-{2,})+\s*\|?\s*$/.test(line);
+
+  // Find separator line (contains only dashes/pipes)
+  const separatorIndex = lines.findIndex(isSeparatorLine);
@@
-    // Stop if we hit another separator or empty line
-    if (line.includes('|') && /[-]+/.test(line) && !line.includes('a')) {
+    // Stop if we hit another separator or empty line
+    if (isSeparatorLine(line)) {
       break;
     }
🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/src/cli/parser.ts` around lines 20 - 41, The
separator detection is too permissive and treats data rows containing hyphens
(e.g., dates) as the table separator; update the checks around separatorIndex
discovery and the loop break condition (the findIndex call, and the in-loop
condition that currently uses line.includes('|') && /[-]+/.test(line) &&
!line.includes('a')) to use a stricter "table divider" regex that only matches
lines composed of pipes, dashes, colons and whitespace (no letters/numbers),
e.g., replace that boolean check with a test like "line matches a regex that
allows only |, -, :, and whitespace" so headerLine, headers,
parseTableWithoutSeparator and the loop over lines correctly treat real
separator rows and do not truncate result rows.

Comment on lines +97 to +103
child.on('close', (code: number | null) => {
resolve({
stdout,
stderr,
exitCode: code ?? 0,
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n plugins/databricks-devtools/src/cli/runner.ts | head -120 | tail -40

Repository: baleen37/claude-plugins

Length of output: 1310


🌐 Web query:

Node.js child_process 'close' event: what does code === null mean when process terminated by signal

💡 Result:

In the child_process 'close' event, code === null means the child did not exit “normally” (i.e., it didn’t return an integer exit status). Instead, it was terminated by a signal—in that case the second argument, signal, is the signal name (e.g., 'SIGTERM', 'SIGKILL'). Node guarantees that exactly one of code or signal will be non-null. [1]

Example:

child.on('close', (code, signal) => {
  // If killed by signal:
  //   code === null
  //   signal === 'SIGTERM' (for example)
});

[1]


🏁 Script executed:

rg "runCommand" plugins/databricks-devtools/src --type ts -B 2 -A 2

Repository: baleen37/claude-plugins

Length of output: 1458


🏁 Script executed:

rg "exitCode" plugins/databricks-devtools/src --type ts -B 3 -A 3

Repository: baleen37/claude-plugins

Length of output: 931


🏁 Script executed:

grep -n "runCommand" plugins/databricks-devtools/src/mcp/server.ts -A 5 -B 2

Repository: baleen37/claude-plugins

Length of output: 584


Reject on signal termination instead of masking as success.

When code === null, the process was terminated by a signal rather than exiting normally. The current code treats this as exitCode: 0, which misleads callers into thinking the command succeeded. The close event provides a second parameter with the signal name; use it to reject the promise when the process is killed.

Safer close handling
-    child.on('close', (code: number | null) => {
-      resolve({
-        stdout,
-        stderr,
-        exitCode: code ?? 0,
-      });
-    });
+    child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
+      if (code === null) {
+        reject(new Error(`Databricks process terminated by signal: ${signal ?? 'unknown'}`));
+        return;
+      }
+      resolve({
+        stdout,
+        stderr,
+        exitCode: code,
+      });
+    });
🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/src/cli/runner.ts` around lines 97 - 103, The
close handler on the child process currently masks signal termination by
treating code === null as exitCode: 0; update the child.on('close', ...)
callback (in runner.ts where the spawned child is referenced as child and the
surrounding promise uses resolve/reject) to accept the second parameter (signal)
and, when code === null, reject the promise with an Error that includes the
signal name and context (instead of resolving with exitCode 0); only resolve
with { stdout, stderr, exitCode: code } for non-null codes. Ensure you reference
the same child and resolve/reject variables so callers receive a rejection on
signal termination.

Comment on lines +23 to +37
const sectionMatch = trimmedLine.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
if (currentProfile) {
profiles[currentProfile] = currentConfig;
}

const profileName = sectionMatch[1];
if (profileName !== 'DEFAULT') {
currentProfile = profileName;
currentConfig = {};
} else {
currentProfile = null;
currentConfig = {};
}
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Databricks CLI configuration file [DEFAULT] section profile

💡 Result:

In the Databricks CLI, the [DEFAULT] section in ~/.databrickscfg is simply the configuration profile named DEFAULT. It’s the profile the CLI uses automatically when you don’t pass --profile / -p. [1][2]

Example (~/.databrickscfg):

[DEFAULT]
host  = https://<workspace-url>
token = <access-token>

[DEV]
host  = https://<dev-workspace-url>
token = <dev-token>

You can select a non-default profile by running commands with --profile DEV (or set DATABRICKS_CONFIG_PROFILE=DEV). [1][3]

Related: the config file location can be overridden with DATABRICKS_CONFIG_FILE. [1][2]

Sources
[1] Databricks CLI configure command docs: (docs.databricks.com)
[2] Databricks CLI configuration profiles overview: (docs.databricks.com)
[3] Databricks auth/config profiles docs (profile usage + DATABRICKS_CONFIG_PROFILE): (docs.databricks.com)

Citations:


🏁 Script executed:

# Find the functions mentioned in the review comment
rg -n "list_profiles|get_profile_info" plugins/databricks-devtools/src/

Repository: baleen37/claude-plugins

Length of output: 394


🏁 Script executed:

# Get more context around the parsing code to understand the full structure
cat -n plugins/databricks-devtools/src/config/databrickscfg.ts | head -60

Repository: baleen37/claude-plugins

Length of output: 2158


🏁 Script executed:

# Get the rest of the file to see if the last profile is saved
tail -n +60 plugins/databricks-devtools/src/config/databrickscfg.ts

Repository: baleen37/claude-plugins

Length of output: 171


🏁 Script executed:

# Also check the list_profiles and get_profile_info implementations to understand their behavior
sed -n '188,210p' plugins/databricks-devtools/src/mcp/server.ts

Repository: baleen37/claude-plugins

Length of output: 618


🏁 Script executed:

# Find the listProfilesTool and getProfileInfoTool implementations
rg -n "listProfilesTool|getProfileInfoTool" plugins/databricks-devtools/src/ -A 5

Repository: baleen37/claude-plugins

Length of output: 2052


🏁 Script executed:

# Also check the types to understand ConfigData and ProfileConfig
cat -n plugins/databricks-devtools/src/config/types.ts

Repository: baleen37/claude-plugins

Length of output: 465


🏁 Script executed:

# Get the full listProfilesTool implementation
sed -n '23,72p' plugins/databricks-devtools/src/mcp/server.ts

Repository: baleen37/claude-plugins

Length of output: 1213


🏁 Script executed:

# Get the full getProfileInfoTool implementation
sed -n '73,130p' plugins/databricks-devtools/src/mcp/server.ts

Repository: baleen37/claude-plugins

Length of output: 1151


Don't drop the DEFAULT profile.

Lines 30-36 skip DEFAULT when parsing, which prevents it from being added to the profiles dictionary. Since [DEFAULT] is the standard Databricks CLI profile (used as the default when no --profile is specified), this breaks list_profiles() (returns empty if only DEFAULT exists) and get_profile_info('DEFAULT') (fails with "not found").

🛠️ Suggested fix (treat DEFAULT as a profile)
       const profileName = sectionMatch[1];
-      if (profileName !== 'DEFAULT') {
-        currentProfile = profileName;
-        currentConfig = {};
-      } else {
-        currentProfile = null;
-        currentConfig = {};
-      }
+      currentProfile = profileName;
+      currentConfig = {};
       continue;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sectionMatch = trimmedLine.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
if (currentProfile) {
profiles[currentProfile] = currentConfig;
}
const profileName = sectionMatch[1];
if (profileName !== 'DEFAULT') {
currentProfile = profileName;
currentConfig = {};
} else {
currentProfile = null;
currentConfig = {};
}
continue;
const sectionMatch = trimmedLine.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
if (currentProfile) {
profiles[currentProfile] = currentConfig;
}
const profileName = sectionMatch[1];
currentProfile = profileName;
currentConfig = {};
continue;
🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/src/config/databrickscfg.ts` around lines 23 -
37, The parser currently skips adding the DEFAULT profile by setting
currentProfile to null when profileName === 'DEFAULT'; change this so DEFAULT is
treated like any other profile: when sectionMatch yields profileName (including
'DEFAULT') set currentProfile = profileName and currentConfig = {}, and ensure
that when switching sections you save profiles[currentProfile] = currentConfig
as done for other profiles (symbols: sectionMatch, profileName, currentProfile,
currentConfig, profiles); this will allow list_profiles() and
get_profile_info('DEFAULT') to find the DEFAULT entry.

Comment on lines +23 to +71
export async function listProfilesTool(): Promise<string> {
const configPath = getDefaultConfigPath();

let profiles: Record<string, ProfileConfig>;
try {
const config = await parseDatabricksConfig(configPath);
profiles = config.profiles;
} catch (error) {
return JSON.stringify(
{
profiles: [],
message: `Failed to read Databricks config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
);
}

const profileNames = Object.keys(profiles);
const profilesWithValidity: Array<{ name: string; host?: string; valid: boolean }> = [];

for (const name of profileNames) {
const profile = profiles[name];
let valid = false;

try {
await runCommand(['workspace', 'list', '/'], { profile: name });
valid = true;
} catch {
valid = false;
}

profilesWithValidity.push({
name,
host: profile.host,
valid,
});
}

return JSON.stringify(
{
profiles: profilesWithValidity,
count: profilesWithValidity.length,
config_path: configPath,
},
null,
2
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect runner error shaping to confirm error messages are safe to surface.
rg -n -C3 "runCommand|DatabricksNotFoundError|stderr" plugins/databricks-devtools/src/cli/runner.ts | head -100

Repository: baleen37/claude-plugins

Length of output: 1684


🏁 Script executed:

#!/bin/bash
# Get the full runCommand function to understand error handling
sed -n '67,110p' plugins/databricks-devtools/src/cli/runner.ts

Repository: baleen37/claude-plugins

Length of output: 1105


🏁 Script executed:

#!/bin/bash
# Check the full listProfilesTool function to see if exitCode is checked
sed -n '23,71p' plugins/databricks-devtools/src/mcp/server.ts

Repository: baleen37/claude-plugins

Length of output: 1212


Check the CLI exit code before marking validation as successful.
Lines 48–50 ignore the CommandResult returned by runCommand(), meaning a non-zero exit code (validation failure) is silently treated as valid: true. Additionally, caught exceptions are swallowed without surfacing actionable error details to the user.

Update the structure to include both exit code validation and error message capture:

♻️ Suggested adjustment
-  const profilesWithValidity: Array<{ name: string; host?: string; valid: boolean }> = [];
+  const profilesWithValidity: Array<{ name: string; host?: string; valid: boolean; error?: string }> = [];
@@
     try {
-      await runCommand(['workspace', 'list', '/'], { profile: name });
+      const result = await runCommand(['workspace', 'list', '/'], { profile: name });
+      valid = result.exitCode === 0;
+      if (!valid && result.stderr) {
+        errorMessage = result.stderr.trim();
+      }
-      valid = true;
-    } catch {
+    } catch (error) {
       valid = false;
+      errorMessage = error instanceof Error ? error.message : String(error);
     }
@@
     profilesWithValidity.push({
       name,
       host: profile.host,
       valid,
+      error: errorMessage,
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function listProfilesTool(): Promise<string> {
const configPath = getDefaultConfigPath();
let profiles: Record<string, ProfileConfig>;
try {
const config = await parseDatabricksConfig(configPath);
profiles = config.profiles;
} catch (error) {
return JSON.stringify(
{
profiles: [],
message: `Failed to read Databricks config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
);
}
const profileNames = Object.keys(profiles);
const profilesWithValidity: Array<{ name: string; host?: string; valid: boolean }> = [];
for (const name of profileNames) {
const profile = profiles[name];
let valid = false;
try {
await runCommand(['workspace', 'list', '/'], { profile: name });
valid = true;
} catch {
valid = false;
}
profilesWithValidity.push({
name,
host: profile.host,
valid,
});
}
return JSON.stringify(
{
profiles: profilesWithValidity,
count: profilesWithValidity.length,
config_path: configPath,
},
null,
2
);
}
export async function listProfilesTool(): Promise<string> {
const configPath = getDefaultConfigPath();
let profiles: Record<string, ProfileConfig>;
try {
const config = await parseDatabricksConfig(configPath);
profiles = config.profiles;
} catch (error) {
return JSON.stringify(
{
profiles: [],
message: `Failed to read Databricks config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
);
}
const profileNames = Object.keys(profiles);
const profilesWithValidity: Array<{ name: string; host?: string; valid: boolean; error?: string }> = [];
for (const name of profileNames) {
const profile = profiles[name];
let valid = false;
let errorMessage: string | undefined = undefined;
try {
const result = await runCommand(['workspace', 'list', '/'], { profile: name });
valid = result.exitCode === 0;
if (!valid && result.stderr) {
errorMessage = result.stderr.trim();
}
} catch (error) {
valid = false;
errorMessage = error instanceof Error ? error.message : String(error);
}
profilesWithValidity.push({
name,
host: profile.host,
valid,
error: errorMessage,
});
}
return JSON.stringify(
{
profiles: profilesWithValidity,
count: profilesWithValidity.length,
config_path: configPath,
},
null,
2
);
}
🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/src/mcp/server.ts` around lines 23 - 71, The
validation currently treats any runCommand call as success if it doesn't throw;
update listProfilesTool to inspect the CommandResult returned by
runCommand(['workspace','list','/'], { profile: name }) instead of only relying
on absence of exceptions: store the result (e.g., const result = await
runCommand(...)), set valid = result.exitCode === 0, and when exitCode !== 0 or
an exception is thrown capture the stderr/stdout or error message into the
profile entry (or include an error field) so the JSON output surfaces actionable
error details rather than swallowing them; reference runCommand and
listProfilesTool to locate where to change the logic.

Comment on lines +73 to +111
export async function getProfileInfoTool(profile: string): Promise<string> {
const configPath = getDefaultConfigPath();

let profiles: Record<string, ProfileConfig>;
try {
const config = await parseDatabricksConfig(configPath);
profiles = config.profiles;
} catch (error) {
return JSON.stringify(
{
error: `Failed to read Databricks config: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
);
}

const profileConfig = profiles[profile];

if (!profileConfig) {
return JSON.stringify(
{
error: `Profile "${profile}" not found in ${configPath}`,
available_profiles: Object.keys(profiles),
},
null,
2
);
}

return JSON.stringify(
{
name: profile,
...profileConfig,
},
null,
2
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact secrets in tool output.
Lines 103-107 return token (and any other secret keys) verbatim. This risks leaking credentials to MCP clients/logs. Please mask or omit secrets by default.

🛡️ Proposed fix (basic redaction)
-  return JSON.stringify(
-    {
-      name: profile,
-      ...profileConfig,
-    },
+  const sanitizedConfig = { ...profileConfig };
+  if (sanitizedConfig.token) {
+    sanitizedConfig.token = '[REDACTED]';
+  }
+  return JSON.stringify(
+    {
+      name: profile,
+      ...sanitizedConfig,
+    },
     null,
     2
   );
🤖 Prompt for AI Agents
In `@plugins/databricks-devtools/src/mcp/server.ts` around lines 73 - 111,
getProfileInfoTool currently returns profileConfig verbatim which can leak
secrets (e.g., token); modify getProfileInfoTool so that before JSON.stringify
it sanitizes profileConfig by removing or masking sensitive keys (e.g., token,
password, secret, access_token, secret_key) — implement a small sanitizer used
on the profileConfig (or build a shallow copy) that replaces values for keys
matching a case-insensitive pattern like
/(token|secret|password|access(_)?token|secret_key)/i with a fixed mask such as
"***REDACTED***" (or omit those keys entirely), then return the sanitized object
instead of the raw profileConfig.

Comment thread plugins/databricks-devtools/tests/cli/runner.test.ts
Implement databricks-devtools plugin for Claude Code that wraps the
Databricks CLI to provide workspace management and SQL execution.

Features:
- CLI command execution with output parsing
- ~/.databrickscfg configuration file parsing
- MCP tools: list_profiles, get_profile_info
- Commands: /databricks, /databricks:sql
- Skill for usage guidance
- 30 tests covering all core functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@baleen37
baleen37 merged commit 7baf422 into main Feb 5, 2026
5 checks passed
@baleen37
baleen37 deleted the feat/databricks branch February 5, 2026 04:36
baleen-release-app Bot pushed a commit that referenced this pull request Feb 5, 2026
# [5.0.0](v4.3.0...v5.0.0) (2026-02-05)

### Bug Fixes

* **ci:** disable strict status checks to allow semantic-release commits ([7b296a4](7b296a4))
* **ci:** merge Release workflow into CI workflow with proper dependency ([906673e](906673e))
* **ci:** trigger CI on release-please PRs using workflow_run ([33ff7d3](33ff7d3))
* **ci:** update bats-action to latest commit ([106b1f3](106b1f3))
* **ci:** update branch protection to match GitHub Actions status check format ([13efcf5](13efcf5))
* **ci:** use full commit SHA for bats-action ([9a70f52](9a70f52))
* **commands:** correct sdd command skill reference ([#403](#403)) ([a4db99c](a4db99c))
* enable glob pattern for plugin.json version updates ([86fd628](86fd628))
* **handoff:** output notification to stderr to exclude from LLM context ([463bb08](463bb08))
* **marketplace:** remove databricks plugin reference ([f0bc6b8](f0bc6b8))
* **ralph-loop:** fix hooks matcher and stdin handling ([326a570](326a570))
* **release:** add owner parameter to GitHub App token generation ([#401](#401)) ([b910a20](b910a20))
* **release:** remove GitHub App token generation ([ced7b17](ced7b17))
* **release:** restore GitHub App token generation ([45dd490](45dd490))
* **release:** use GitHub App token with Administration permission ([4077ecd](4077ecd))
* **release:** use GitHub App token with Administration permission ([#411](#411)) ([e6b1e9c](e6b1e9c))
* **release:** use GITHUB_TOKEN directly with github-actions[bot] bypass ([c7b517c](c7b517c))
* **release:** use GITHUB_TOKEN instead of custom GitHub App ([76a2a00](76a2a00))
* **release:** use secrets.GITHUB_TOKEN directly ([a2aaf50](a2aaf50))
* set release-please manifest to start from 4.3.0 ([#377](#377)) ([23a6cba](23a6cba))

### Features

* **bash-lsp:** add Bash Language Server plugin ([#394](#394)) ([7770f3d](7770f3d))
* **ci:** replace release-please with semantic-release ([35d346f](35d346f))
* **conversation-memory:** add automatic npm install when node_modules missing ([db55514](db55514))
* **conversation-memory:** add file-based logging system ([81f65bd](81f65bd))
* **conversation-memory:** add marked dependency for markdown rendering ([1887174](1887174))
* **conversation-memory:** improve MCP installation error handling ([#373](#373)) ([16eb7b7](16eb7b7))
* **conversation-memory:** reduce token usage in summarization ([d921bc7](d921bc7))
* **conversation-memory:** reduce token usage in summarization ([b3a245b](b3a245b))
* **databricks-devtools:** add Databricks CLI wrapper plugin ([#408](#408)) ([7baf422](7baf422))
* **handoff:** add session handoff plugin for context transfer ([#374](#374)) ([35cfb0c](35cfb0c))
* **lsp-support:** add Kotlin, Lua, Nix LSP servers, merge bash-lsp ([8f1ee4f](8f1ee4f))
* **me:** add commit skill ([8d0cf1c](8d0cf1c))
* **me:** add tdd and spawn commands ([#371](#371)) ([9af9bbe](9af9bbe))
* migrate from semantic-release to Release Please ([#375](#375)) ([d20c736](d20c736))
* **release-please:** add auto-merge for release PRs ([cef4678](cef4678))
* **test-helpers:** extract shared BATS helpers to @baleen/bats-helpers package ([19f2a62](19f2a62))
* **tests:** run plugin tests in parallel using background processes ([04be773](04be773))

### Performance Improvements

* **tests:** add caching to improve test execution speed ([9bbee5b](9bbee5b))

### BREAKING CHANGES

* Release workflow now requires merging Release Please PRs
instead of automatic releases on push to main.

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant