Plugin hooks

June 22, 2026 · View on GitHub

Three hooks make sure Databricks work flows through the skills. All are stdlib-only Python and fail open (any error prints {} / no output and exits 0, so a broken hook never blocks a prompt, session start, or tool call). hooks.json wires them in; Claude Code expands ${CLAUDE_PLUGIN_ROOT}. Claude Code auto-loads hooks/hooks.json, so it is not declared in plugin.json (declaring the standard path double-loads it and fails the plugin).

The four hook-wiring files (hooks.json, codex-hooks.json, copilot-hooks.json, cursor-hooks.json) are generated from metaplugin/plugin.meta.json (the hooks block + each target's hooks_render) by scripts/skills.py. They are the same three logical hooks rendered into each runtime's dialect (schema shape, event-name casing, env var, command form, tool matcher, and whether the router is wired). To change hook wiring, edit metaplugin/plugin.meta.json and run python3 scripts/skills.py generate; do not hand-edit these JSON files (CI fails on drift). The hook scripts (*.py) are hand-written; only the wiring JSON is generated.

In the per-provider bundle, each provider ships its dialect's wiring at the path that runtime auto-discovers: Claude, Codex, and Cursor use plugins/databricks/<provider>/hooks/hooks.json, while Copilot-format plugins use plugins/databricks/copilot/hooks.json at the provider root. Hook scripts still live under hooks/. No plugin.json declares a "hooks" path (declaring an auto-discovered file would double-load it). The four distinct names above exist only at the repo root so the single generation step doesn't collide.

Changing or adding a hook

  • Change a hook's behavior: edit its *.py script. There is one shared copy per script, so the change applies to every target at once.
  • Change a hook's wiring (e.g. the tool matcher or env-var root): edit that target's hooks_render block in metaplugin/plugin.meta.json, then python3 scripts/skills.py generate.
  • Add a new hook: add it to hooks.entries in metaplugin/plugin.meta.json (an id + script) and extend the dialect builders in scripts/skillsgen/hooks.pybuild_nested_hooks (Claude/Codex), build_copilot_hooks, build_cursor_hooks — to place the new event in each runtime's dialect. The builders set event placement per platform in code (not from entries[].event, which is currently descriptive only) because the four runtimes differ in event-name casing, schema shape, command form, and which hooks are wired (the prompt router runs only on Claude + Codex). So adding a hook is a small per-dialect code change, not just a data edit. check_no_orphan_hook_scripts (run by validate) fails if a hooks/*.py is left unwired.

cursor-hooks.json is the Cursor-dialect wiring; the bundle ships it as the Cursor folder's hooks/hooks.json, which Cursor auto-discovers from the plugin root (no "hooks" declaration). It runs the context primer (sessionStart) and the auth hinter (postToolUse, matcher Shell) with --platform cursor, which switches the scripts' output envelope to Cursor's additional_context and renders the Cursor command names (/databricks-setup, /databricks-doctor) in the injected text. The prompt-router hook is not wired on Cursor (beforeSubmitPrompt can block a prompt but cannot inject context). Routing instead ships as a Cursor rule (rules/databricks-routing.mdc, declared via "rules" in .cursor-plugin/plugin.json): an Apply-Intelligently rule whose description triggers it on Databricks-related prompts and injects the same product-skill table the Claude/Codex router uses. This is load-bearing because Cursor currently drops additional_context from sessionStart/postToolUse for non-MCP tools (open upstream bug, Cursor forum #155689 / #158452), so the primer and auth-hint hooks are effectively no-ops on Cursor today; the rule loads through the rules engine, not the hook-injection path, so it is unaffected. Native skill selection also helps.

copilot-hooks.json is the GitHub Copilot wiring; the bundle ships it as the Copilot folder's root hooks.json, which Copilot-format plugins auto-discover (no "hooks" declaration). It uses PascalCase event names, which selects Copilot's Claude-compatible payload dialect, so the scripts run unchanged and emit the Claude output envelope. Only two hooks are wired: the context primer (SessionStart) and the auth hinter (PostToolUse). Copilot's hooks run on the Copilot CLI and the cloud agent (VS Code ships its own separate hooks system); the CLI injects SessionStart / PostToolUse additionalContext as of Copilot CLI v1.0.11 (earlier versions dropped session-start output). The prompt router is not wired: no Copilot surface lets a prompt-submit hook inject context (userPromptSubmitted output is not processed), so routing rides on skill descriptions and instruction files. Each entry carries bash and powershell command variants per Copilot's hook format.

codex-hooks.json is the Codex-dialect wiring; the bundle ships it as the Codex folder's hooks/hooks.json, Codex's default plugin hook file, which it auto-discovers from the plugin root (no "hooks" declaration). Codex uses Claude's event names and payload/output schema, so all three scripts run unchanged. Script paths resolve via the PLUGIN_ROOT env var Codex exports to hook processes (with CLAUDE_PLUGIN_ROOT as fallback; Codex exports both). Codex hash-pins plugin hooks: users approve them via /hooks after install and after every update, and enterprises can pre-trust them through managed requirements.toml hooks.

Each hook is pinned by a test file in tests/ at the repo root; run the whole suite with python3 -m unittest discover -s tests -p '*_test.py'.

databricks-router.py: prompt router (UserPromptSubmit)

Runs a fast keyword regex (sub-50ms, no LLM, no network) over each user prompt. When the prompt is Databricks-related, it injects an additionalContext instruction telling Claude to load databricks-core plus the matching product skill before answering. When it isn't, it prints {} and stays out of the way.

The full instruction is injected once per session (tracked by a marker file in the temp dir keyed on the payload's session_id); later Databricks prompts in the same session get a one-line reminder instead, so long sessions don't pay the full routing block on every turn.

There's no second agent to delegate to. Claude itself drives the databricks CLI through the skills, so "routing" just means "make sure the Databricks skills are loaded." There is no permission gating and no cost warning here.

Precision is tuned to avoid over-routing:

  • STRONG terms (databricks, unity catalog, lakeflow, dbfs, databricks.yml, spark declarative pipelines, delta live tables (the legacy name still routes), ...) always route, even alongside an alternative-platform mention, so "migrate from redshift to databricks" routes.
  • AMBIGUOUS terms (declarative pipelines, model serving, vector search, mlflow, pyspark, genie, ...) route only when no SUPPRESS term is present.
  • SUPPRESS terms (alternative data platforms, Jenkins, and plainly-local dev work like git commit, read the file, unit test, npm) hold back an ambiguous match.
  • URLs: code-hosting URLs are blanked before matching, so databricks appearing only as a GitHub/GitLab org or repo name (github.com/databricks/...) does not route. URLs whose hostname contains databricks (workspace and docs hosts) still do.

These three lists, plus the injected instruction, live in metaplugin/plugin.meta.json's routing block and are generated into _routing_data.json (next to this script), which the router loads at import. To change them, edit metaplugin/plugin.meta.json and run python3 scripts/skills.py generate; do not hand-edit _routing_data.json. If that file is ever missing or unreadable the router falls back to a minimal inline config that routes only literal databricks mentions (fail-open), so an install that loses it keeps working but loses product-keyword routing until it is restored. Behavior is pinned by tests/databricks_router_test.py.

databricks-context.py: context primer (SessionStart)

Injects a compact banner at session start: the routing rule (load databricks-core + the product skill), CLI presence + version, configured profile names plus any [__settings__].default_profile (parsed from ~/.databrickscfg locally, no network call, token values never printed), and whether env/in-platform auth is set. If the CLI isn't installed it points at /databricks:setup. Covered by tests/databricks_context_test.py.

Its hooks.json entry uses "matcher": "startup|clear|compact": the banner fires for new sessions, /clear, and after compaction, but not on resume, where the prior context already contains it.

databricks-auth-helper.py: auth-failure hint (PostToolUse)

Watches Bash tool results (matcher: Bash). When a databricks command's output matches a phrase-shaped auth-failure signal (missing default credentials, invalid_grant, 401 unauthorized, invalid/expired token), it injects one line suggesting /databricks:doctor or databricks auth login before any retry. It never blocks or rewrites tool calls; bare status codes in ordinary output do not trigger it. Only commands that actually invoke the databricks executable count: databricks appearing as a repo path, URL, or argument (gh pr view --repo databricks/cli) does not, since such output can legitimately quote auth-failure phrases without any auth problem. Covered by tests/databricks_auth_helper_test.py.

Distribution note

These ship inside each provider's bundle folder under plugins/databricks/<provider>/ that that agent fetches (each marketplace.json catalog points a scoped source at its own subfolder): with the Claude Code plugin, with the GitHub Copilot plugin (primer + auth hinter via copilot-hooks.json, catalogued in .github/plugin/marketplace.json), and with the Codex plugin (all three hooks via codex-hooks.json, catalogued in .agents/plugins/marketplace.json). Only the hook scripts a provider's wiring references are copied into its folder. The Cursor marketplace plugin (databricks) ships the context primer and auth hinter via cursor-hooks.json plus the routing rule (rules/databricks-routing.mdc); the router hook stays Claude/Codex-only. The Copilot cloud agent takes no plugins; it only reads hooks vendored into the target repo's .github/hooks/. The Databricks CLI install path (databricks aitools install) currently packages skills only. See the repo README for the parity follow-up.