ClaudeSuperPower
P

PostToolUse (mcp__.*__find_columns|mcp__.*__get_dataset_columns|mcp__.*__get_dataset)

Hook

Pre-query column validation via schema caching. Catches unknown column errors before they hit the API by building a per-session schema cache from find_columns / get_dataset_columns / get_dataset results.

Fires on

PostToolUsematchingmcp__.*__find_columns|mcp__.*__get_dataset_columns|mcp__.*__get_dataset

Immediately after a tool returns, with its result available.

Where this sits in a session

  1. Session start
  2. Prompt submitted
  3. Prompt expansion
  4. Before tool use
  5. After tool use
  6. Notification
  7. Before compaction
  8. Subagent finished
  9. Turn finished
  10. Session end

Observes onlyEmits messages or context. It never returns a permission decision.

What it runs

Executed automatically when the event fires — no prompt, no confirmation.

command· timeout 5s
bash ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/cache-columns.sh

Install it yourself

Paste into .claude/settings.json to run this hook without installing the whole plugin. Adjust the paths to point at your own copy of the scripts.

settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/cache-columns.sh",
            "timeout": 5
          }
        ],
        "matcher": "mcp__.*__find_columns|mcp__.*__get_dataset_columns|mcp__.*__get_dataset"
      }
    ]
  }
}

Source

Read this before installing — it runs on your machine with your permissions.

#!/usr/bin/env bash
set -euo pipefail

# PostToolUse hook for find_columns / get_dataset_columns.
#
# Caches column names from the tool result into a per-session temp file.
# The validate-query.sh PreToolUse hook checks this cache before run_query
# to catch unknown column names before they hit the API.
#
# Cache location: $TMPDIR/honeycomb-schema/$session_id/$env--$dataset.txt
# One column name per line, sorted and deduplicated.

input=$(cat)

env_slug=$(echo "$input" | jq -r '.tool_input.environment_slug // empty')
dataset_slug=$(echo "$input" | jq -r '.tool_input.dataset_slug // empty')
session_id=$(echo "$input" | jq -r '.session_id // "default"')
tool_name=$(echo "$input" | jq -r '.tool_name // empty')

# MCP PostToolUse responses use the CallToolResult object shape
# (`{"content":[{"type":"text","text":"..."}]}`), while older Claude
# versions exposed the content blocks as a top-level array. Accept both, plus
# direct text objects/strings, and fail open for response types we do not know.
tool_result=$(
  printf '%s\n' "$input" | jq -r '
    .tool_response
    | if type == "array" then .
      elif type == "object" then
        if (.content? | type) == "array" then .content else [.] end
      elif type == "string" then [.]
      else []
      end
    | map(
        if type == "object" then (.text? // empty)
        elif type == "string" then .
        else empty
        end
      )
    | map(select(type == "string"))
    | join("\n")
  ' 2>/dev/null
) || tool_result=""

# Environment and result required — fail open if missing
if [[ -z "$env_slug" || -z "$tool_result" ]]; then
  exit 0
fi

# If no dataset specified, use "_all" as a cross-dataset cache
if [[ -z "$dataset_slug" ]]; then
  dataset_slug="_all"
fi

cache_dir="${TMPDIR:-/tmp}/honeycomb-schema/${session_id}"
mkdir -p "$cache_dir"
cache_file="${cache_dir}/${env_slug}--${dataset_slug}.txt"

# Parse column names from markdown table output.
# Both find_columns and get_dataset_columns return pipe-delimited tables
# with the column name in the first data column:
#   | Name | Type | Description | ...
#   |------|------|-------------|
#   | app.team_id | integer | ... |
#
# Strategy: grab rows with pipes, skip the header and separator,
# extract the first data cell.
echo "$tool_result" \
  | grep -E '^\s*\|' \
  | grep -vE '^\s*\|\s*Name\s*\|' \
  | grep -vE '^\s*\|\s*-' \
  | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); if ($2 != "" && $2 !~ /^[- ]+$/) print $2}' \
  >> "$cache_file" 2>/dev/null || true

# Deduplicate
if [[ -f "$cache_file" ]]; then
  sort -u "$cache_file" -o "$cache_file"
fi

# Mark the cache "complete" only when we actually hold the FULL schema.
# find_columns returns only its top-N matches, so its cache is always partial
# (no marker). get_dataset_columns paginates: one response is the whole schema
# only when total_pages <= 1; for multi-page schemas we record which pages we've
# cached and mark complete once every page has been seen. validate-query.sh keys
# its nudge firmness off this marker, so marking complete while pages are still
# un-fetched would nudge against columns that genuinely exist on later pages.
if [[ "$tool_name" == *get_dataset_columns* ]]; then
  # `page:` is anchored to line-start so it doesn't also match `items_per_page:`.
  total_pages=$(printf '%s\n' "$tool_result" | grep -E '^[[:space:]]*total_pages:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1 || true)
  page=$(printf '%s\n' "$tool_result" | grep -E '^[[:space:]]*page:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1 || true)
  : "${total_pages:=1}"
  : "${page:=1}"

  complete_marker="${cache_file%.txt}.complete"
  if [[ "$total_pages" -le 1 ]]; then
    # Single page is the entire schema.
    touch "$complete_marker"
  else
    # Multi-page: track distinct pages seen; complete once all are cached.
    pages_file="${cache_file%.txt}.pages"
    echo "$page" >> "$pages_file"
    sort -un "$pages_file" -o "$pages_file"
    seen=$(wc -l < "$pages_file" | tr -d '[:space:]')
    if [[ "$seen" -ge "$total_pages" ]]; then
      touch "$complete_marker"
    fi
  fi
fi

exit 0

Shipped by 1 plugin

Installing any of these installs this hook.

Reviews

Log in to leave a review.

No reviews yet — be the first.

Explore related

Other things in this space — across every part of the ecosystem, not just hooks.