Skip to content

feat(plugin): add prompt-routing hooks and doctor/setup commands#128

Open
simonfaltum wants to merge 10 commits into
mainfrom
simonfaltum/plugin-hooks-commands
Open

feat(plugin): add prompt-routing hooks and doctor/setup commands#128
simonfaltum wants to merge 10 commits into
mainfrom
simonfaltum/plugin-hooks-commands

Conversation

@simonfaltum

@simonfaltum simonfaltum commented Jun 8, 2026

Copy link
Copy Markdown
Member

Why

The plugin shipped skills only. The skills carry the Databricks judgment, but nothing made sure a Databricks-related prompt actually reached them, and there were no entry points for common setup or health-check tasks. This adds a thin hook and command layer so Databricks work routes into the skills automatically, staying CLI-first (no MCP).

What the hooks do (plain language)

The hooks make sure Databricks work goes through the skills instead of Claude improvising:

  • When your prompt mentions something Databricks-related, the router hook adds a short note to that turn telling Claude to load the relevant skill(s) first, and it stays silent on non-Databricks prompts. The full note fires once per session; later Databricks prompts get a one-line reminder, so long sessions don't pay the full block every turn.
  • The context hook runs at session start (new sessions, /clear, and after compaction, but not on resume where the banner is already in context) and injects the routing reminder plus a one-line CLI/profile status.
  • The auth helper hook watches databricks command results; when one fails with an auth-shaped error (expired/invalid token, missing credentials, OAuth refresh failure), it adds one line suggesting /databricks:doctor or databricks auth login before any retry.

All three only inject context that Claude then acts on (technically additionalContext, a just-in-time nudge, not a forced skill load). They never gate, block, or change your commands, there are no permission prompts or cost warnings, and they fail open (any error means no output, never a blocked prompt or tool call).

Changes

Before: skills only. Now: skills + three hooks + two commands.

Hooks (hooks/, auto-loaded via hooks/hooks.json, all fail open, stdlib only):

  • databricks-router.py (UserPromptSubmit): a sub-50ms keyword regex over the prompt. On a Databricks match it injects context steering Claude to load databricks-core plus the matching product skill; the full instruction is injected once per session (marker file keyed on session_id), then a one-line reminder. Unrelated prompts are left untouched. Code-hosting URLs (github.com/databricks/...) do not count as Databricks intent; URLs whose hostname contains databricks (workspace, docs) do.
  • databricks-context.py (SessionStart, matcher startup|clear|compact): injects the routing rule plus CLI version, configured profile names, and any [__settings__].default_profile (read locally, no network call, no token values).
  • databricks-auth-helper.py (PostToolUse, matcher Bash): phrase-based auth-failure detection on databricks command output (cannot configure default credentials, invalid_grant, 401 unauthorized, invalid/expired token); injects a one-line fix suggestion. Bare status codes in ordinary data never trip it, and only commands that actually invoke the databricks executable count (a repo path or URL containing databricks, like gh pr view --repo databricks/cli, does not).

Commands (commands/):

  • /databricks:doctor: read-only health check (CLI, auth method via auth describe, reachability, compute, recent run failures).
  • /databricks:setup: auth onboarding (OAuth / PAT / service principal). Interactive auth commands (PAT paste, browserless OAuth) are handed to the user's own terminal instead of being run without a TTY.

Product workflows stay in the skills, so no command shadows a skill of the same name. plugin.json declares commands; hooks/hooks.json is auto-loaded by Claude Code and intentionally not declared (declaring the standard path double-loads it and fails the plugin).

Tooling: scripts/skills.py validate now validates the components (hooks.json is valid JSON referencing scripts that exist, every command has a description without unquoted-colon YAML pitfalls, and plugin.json does not re-declare the auto-loaded hooks file, string or list form). CI runs all hook unit test files. README, CONTRIBUTING, and CLAUDE.md document the new surface.

Test plan

  • python3 hooks/databricks_router_test.py (strong / ambiguous / suppressed routing, URL handling, once-per-session memo, payload shapes)
  • python3 hooks/databricks_context_test.py (config parsing, default_profile, sanitization, no-CLI and no-profiles banners)
  • python3 hooks/databricks_auth_helper_test.py (auth-failure phrases, executable-token command detection, non-databricks/non-Bash/bare-status negatives)
  • python3 scripts/skills.py validate
  • End-to-end via the hook stdin/stdout contract (router full-then-reminder across invocations, auth hint vs silent, context primer output, fail-open on malformed input)
  • Installed locally and verified in-session: /reload-plugins loads the hooks, the router injects context live, both commands register
  • Command frontmatter parses under strict YAML (PyYAML safe_load)

Distribution & releases

How this reaches users, and how it relates to releases:

  • Claude Code marketplace (databricks/databricks-agent-skills, source: "./"): tracks main, but clients only pick up a change once the version field in .claude-plugin/plugin.json is bumped, which happens only via the release workflow (bump_version.py commits the bump to main and tags vX.Y.Z). This PR intentionally does not bump version (the release workflow owns that), so these hooks/commands land on main at merge but reach marketplace users only after the next release is cut, not on merge.
  • databricks aitools install (CLI path): resolves via cli-compat.json in databricks/cli and currently packages skills only. Shipping hooks/ + commands/ through this path is separate CLI-side follow-up work.

Ship path: merge this, then run the release workflow for a new vX.Y.Z (bumps version + tags), and marketplace clients get it on marketplace update.

This pull request and its description were written by Isaac.

Add a Claude Code hook and command layer on top of the existing skills,
keeping the plugin CLI-first (no MCP).

Hooks (in hooks/, auto-loaded via hooks/hooks.json; both fail open, stdlib only):
- databricks-router.py (UserPromptSubmit): a fast keyword regex that routes
  Databricks-related prompts into databricks-core plus the matching product
  skill. Modeled on Snowflake Cortex Code. No permission gating, no cost
  warnings.
- databricks-context.py (SessionStart): primes the routing rule and reports
  CLI version and configured profile names (local only, no network, no token
  values).

Commands (in commands/): /databricks:doctor (read-only health check) and
/databricks:setup (auth onboarding). Product workflows stay in the skills, so
no command shadows a skill of the same name.

Tooling: scripts/skills.py validate now checks the components (valid hooks.json
referencing scripts that exist, every command has a description, and plugin.json
does not double-declare the auto-loaded hooks file). CI runs the router unit
test. README, CONTRIBUTING, and CLAUDE.md document the new surface.

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
@simonfaltum simonfaltum requested review from a team and lennartkats-db as code owners June 8, 2026 14:29
Remove the competitor attribution from the prompt-router docstrings, READMEs, and PR-facing docs, and drop snowflake/cortex from the router's SUPPRESS keyword list and tests. The router is just a keyword matcher, so no attribution is needed. Other competitor suppressors (bigquery, redshift, synapse) stay, and routing behavior for Databricks prompts is unchanged (8/8 tests pass).

Co-authored-by: Isaac
… router + validator tuning)

- hooks.json: append `|| true` so the hook is fully fail-open even when neither
  python3 nor python can run the script.
- databricks-context.py: only read the config file when it is a regular file
  under a 1MB cap, so a FIFO/device/huge file pointed at by
  DATABRICKS_CONFIG_FILE can never hang or do unbounded work at session start;
  sanitize injected profile names and DATABRICKS_CONFIG_PROFILE (strip control
  chars/newlines, cap length) so config-derived strings can't inject context.
- databricks-router.py: tune keyword tiers — drop `photon` and move `genie` out
  of STRONG into AMBIGUOUS (common words were over-routing), add `\bdabs?\b` so
  DAB/DABs prompts route, and trim the suppress list.
- scripts/skills.py: require plugin.json to declare `commands` when commands/
  exists (mirror of the no-`hooks`-declaration rule).

Addresses the cursor-agent review of PR #128.

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
The SessionStart hook can't know the latest CLI version offline, and any pinned
floor just goes stale (0.292.0 already had, with the CLI now at 1.2.x). So stop
gating on a version: the context hook reports the detected version only, and the
commands + hooks README no longer cite a specific minimum. CLI currency is left
to the CLI's own update notice; the databricks-core skill stays the single
source of truth for any hard minimum.

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
…e config read

From a skill-review pass over the PR:
- Replace every em-dash in the new hooks, commands, validator error messages, and
  the README/CONTRIBUTING/CLAUDE docs with commas/periods/colons (house style).
  Two of these shipped into runtime: the router's injected instruction and the
  SessionStart banner.
- Narrow the router's `\bdabs?\b` to `\bdabs\b` so a bare "dab" no longer routes.
- Document why the SessionStart hook parses ~/.databrickscfg directly instead of
  calling `databricks auth profiles` (must stay offline and fast).

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
- Quote command frontmatter descriptions (strict YAML rejects unquoted
  colons) and make the validator flag unquoted ':' in command descriptions
- Blank code-hosting URLs in the router before matching, so
  github.com/databricks/... alone does not route; URLs whose hostname
  contains databricks (workspace, docs) still do
- /databricks:setup: hand interactive auth commands to the user's own
  terminal instead of running them without a TTY
- Context hook: use the sanitized config basename in the no-profiles
  message, matching the profiles-found branch
- skills.py validate: clean error on malformed plugin.json or
  marketplace.json, and catch list-form hooks declarations of the
  auto-loaded hooks file
- Add hooks/databricks_context_test.py and run it in CI

Co-authored-by: Isaac
Optimizations:
- Router injects the full routing instruction once per session (marker
  file keyed on session_id); later Databricks prompts get a one-line
  reminder instead of the full block
- SessionStart context primer uses matcher startup|clear|compact so it
  no longer re-injects the banner on resume

Improvements:
- New PostToolUse hook (databricks-auth-helper.py, matcher Bash): when a
  databricks command fails with an auth-shaped error, injects one line
  suggesting /databricks:doctor or databricks auth login; phrase-based
  matching so bare status codes in data never trip it
- Router keywords: delta sharing and cloudFiles (strong), sql warehouse
  and auto loader (ambiguous), snowflake (suppress), and a Genie line in
  the routing instruction
- Context banner surfaces [__settings__].default_profile when set
- Doctor identity step uses databricks auth describe (credential source)
  with current-user as fallback, never --sensitive

Tests for all of the above; CI consolidates the hook test runs into one
step. Docs updated (README, CONTRIBUTING, CLAUDE.md, hooks/README).

Co-authored-by: Isaac
The auth helper matched \bdatabricks\b anywhere in the command string, so
gh/git/curl commands referencing the databricks GitHub org or docs tripped
the hint whenever their output quoted an auth-failure phrase (observed live
on 'gh pr view --repo databricks/databricks-agent-skills', whose PR body
describes this very hook). Detection now splits the command into shell
segments and requires the databricks executable (optionally path-prefixed,
behind env assignments or sudo/env/xargs-style wrappers) to lead a segment.

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
Removing a shipped plugin's entry from marketplace.json does not just stop
updates. Claude Code re-resolves installed plugins against the marketplace
catalog at load time, so existing installs immediately fail to load and lose
all skills, hooks, and commands. Verified empirically in an isolated
CLAUDE_CONFIG_DIR: after removing the entry and refreshing, plugin list shows
'failed to load: Plugin databricks not found in marketplace', and recovery
requires manual uninstall + reinstall. Relevant for the planned official
marketplace listing: that is an additive channel, never a replacement for the
entry here.

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
…er docs

Review feedback (Renaud): this repo is public, and framing the suppression
list as competitor detection invites bad optics for zero functional benefit.
Comments, README, and test names now say alternative platform; the routing
behavior is unchanged.

Co-authored-by: Isaac
Signed-off-by: simon <simon.faltum@databricks.com>
Comment thread commands/doctor.md
Comment on lines +25 to +31
4. **Workspace reach**: `databricks catalogs list --profile <profile>`.
Confirms API reachability and Unity Catalog access.
5. **Compute**: `databricks warehouses list` and `databricks clusters list` for
the profile. Note what's running.
6. **Recent job failures**: list recent job runs (e.g.
`databricks jobs list-runs --limit 20 --profile <profile>`) and surface any
recent failures.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why do we have these checks? What value does it provide? Because if we want to see whether the profile is valid, we can simply test a single API.

Comment thread commands/setup.md
@@ -0,0 +1,40 @@
---
description: "Set up Databricks CLI auth: install check, then an OAuth / PAT / service-principal profile, then verify."
argument-hint: "[workspace-url]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What about spog URLs/account ID?

Comment on lines +115 to +120
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": result,
}
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not sure, but can it error out? To be safe, should we cover the entire main block with a try cache?

Comment on lines +33 to +38

- name: Test plugin hooks
run: |
python3 hooks/databricks_router_test.py
python3 hooks/databricks_context_test.py
python3 hooks/databricks_auth_helper_test.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we have the test in a separate test directory? and run the complete suite with a single command?

@dustinvannoy-db dustinvannoy-db left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment to consider and messaged about a design decision. Nothing blocking this PR from my perspective.

Comment thread hooks/README.md
Precision is tuned to avoid over-routing:

- **STRONG** terms (`databricks`, `unity catalog`, `lakeflow`, `dbfs`,
`databricks.yml`, `delta live tables`, `genie`, ...) always route, even

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should change delta live tables to declarative pipelines

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.

3 participants