Files
vercel__vercel-plugin/docs/skill-injection.md
John Lindquist d5b5ef47f7 Replace skill body injection with Skill tool invocation instructions
Instead of injecting the full SKILL.md body as additionalContext, inject
"You must run the Skill(<name>) tool." — a more conventional way of
telling the agent to use the Skill tool for context loading.
2026-03-10 12:46:11 -06:00

31 KiB
Raw Permalink Blame History

Skill Injection Engine

Audience: Plugin developers, skill authors, and anyone debugging why a skill did or didn't inject.

This document explains the complete skill injection pipeline — how vercel-plugin decides which skills to surface, when, and why. It covers both injection hooks (PreToolUse and UserPromptSubmit), the ranking system with all boost factors, the dedup state machine, budget enforcement, prompt signal scoring, and special-case triggers.


Table of Contents

  1. How Injection Works (Overview)
  2. PreToolUse Pipeline
  3. UserPromptSubmit Pipeline
  4. Prompt Signal Scoring
  5. Ranking & Boost Factors
  6. Dedup State Machine
  7. Budget Enforcement
  8. Special-Case Triggers
  9. User Stories
  10. PostToolUse Validation
  11. Environment Variables Reference

How Injection Works (Overview)

The plugin watches what Claude is doing and injects only the skills relevant to the current action. There are two independent injection paths:

Hook Trigger Budget Max Skills Match Method
PreToolUse Claude calls Read, Edit, Write, or Bash 18 KB 5 File path globs, bash regex, import patterns
UserPromptSubmit User types a prompt 8 KB 2 Prompt signal scoring (phrases/allOf/anyOf/noneOf)

Both hooks share the same dedup system — once a skill is injected, it won't be injected again in the same session.

flowchart LR
    subgraph "Claude Code Session"
        A["User types prompt"] --> B{"UserPromptSubmit<br/>hook"}
        C["Claude calls tool"] --> D{"PreToolUse<br/>hook"}
    end

    B --> E["Prompt Signal<br/>Scoring"]
    D --> F["Pattern<br/>Matching"]

    E --> G["Rank → Dedup → Budget"]
    F --> G

    G --> H["additionalContext<br/>injection"]
    H --> I["Claude sees<br/>skill content"]

PreToolUse Pipeline

The PreToolUse hook fires every time Claude calls Read, Edit, Write, or Bash. It runs a six-stage pipeline:

flowchart TD
    INPUT["stdin JSON<br/>(tool_name, tool_input, session_id)"]
    PARSE["Stage 1: parseInput<br/>Extract toolName, toolTarget, scopeId"]
    LOAD["Stage 2: loadSkills<br/>Manifest (fast) or live scan (fallback)"]
    MATCH["Stage 3: matchSkills<br/>Path globs / bash regex / import patterns"]
    SPECIAL["Stage 3.5: Special Triggers<br/>TSX review (+40) · Dev server (+45) · Env help"]
    RANK["Stage 4: Rank & Deduplicate<br/>vercel.json (±10) · Profiler (+5) · Setup (+50)"]
    INJECT["Stage 5: injectSkills<br/>Budget: 18KB · Ceiling: 3 skills · Summary fallback"]
    FORMAT["Stage 6: formatOutput<br/>JSON with additionalContext"]

    INPUT --> PARSE --> LOAD --> MATCH --> SPECIAL --> RANK --> INJECT --> FORMAT

    style PARSE fill:#e1f5fe
    style LOAD fill:#e8f5e9
    style MATCH fill:#fff3e0
    style SPECIAL fill:#fce4ec
    style RANK fill:#f3e5f5
    style INJECT fill:#fff9c4
    style FORMAT fill:#e0f2f1

Stage 1: Parse Input

Source: pretooluse-skill-inject.mts:parseInput()

Reads JSON from stdin and extracts:

Field Description
toolName One of Read, Edit, Write, Bash
toolInput The tool's arguments (e.g., file_path, command)
sessionId Used for file-based dedup
toolTarget Primary target — file path for file tools, command string for Bash
scopeId Agent ID for subagent-scoped dedup (undefined for main agent)

Unsupported tools (anything not in ["Read", "Edit", "Write", "Bash"]) are rejected with an empty {} response.

Stage 2: Load Skills

Source: pretooluse-skill-inject.mts:loadSkills()

Two-tier loading strategy:

  1. Manifest (generated/skill-manifest.json): Pre-built JSON with pre-compiled regex. Version 2 includes paired arrays (pathPatternspathRegexSources) so hooks reconstruct RegExp objects directly — no glob compilation needed.

  2. Live scan (fallback): Scans skills/*/SKILL.md, parses YAML frontmatter via buildSkillMap(), validates with validateSkillMap(), and compiles patterns at runtime.

The manifest path is always preferred (faster: no filesystem scan, no YAML parsing, no glob compilation).

Stage 3: Match Skills

Source: pretooluse-skill-inject.mts:matchSkills()

For file tools (Read/Edit/Write):

flowchart TD
    FILE["file_path from tool input"] --> FULL{"Full path<br/>matches glob?"}
    FULL -->|Yes| HIT["Match found<br/>(type: full)"]
    FULL -->|No| BASE{"Basename<br/>matches glob?"}
    BASE -->|Yes| HIT2["Match found<br/>(type: basename)"]
    BASE -->|No| SUFFIX{"Suffix<br/>matches glob?"}
    SUFFIX -->|Yes| HIT3["Match found<br/>(type: suffix)"]
    SUFFIX -->|No| IMPORT{"File content<br/>matches importPatterns?"}
    IMPORT -->|Yes| HIT4["Match found<br/>(type: import)"]
    IMPORT -->|No| MISS["No match"]

For Bash: Match command string against each skill's compiled bashPatterns regex.

Each match produces a MatchReason with the winning pattern and match type.

Stage 4: Rank & Deduplicate

Source: pretooluse-skill-inject.mts:deduplicateSkills()

Priority adjustments are applied in layers. See Ranking & Boost Factors for the complete boost table.

Steps:

  1. Filter already-seen — remove skills present in the merged dedup state
  2. vercel.json routing — if target is vercel.json, read keys, adjust priorities (±10)
  3. Profiler boost — skills in VERCEL_PLUGIN_LIKELY_SKILLS get +5
  4. Setup-mode routingbootstrap gets +50 in greenfield projects
  5. Rank — sort by effectivePriority DESC, then skill name ASC
  6. Cap — take the top N skills (default 5)

Stage 5: Inject with Budget Enforcement

Source: pretooluse-skill-inject.mts:injectSkills()

For each ranked skill in priority order:

  1. Check hard ceiling (max 3 skills) — drop with cap_exceeded
  2. Read skills/<name>/SKILL.md, strip frontmatter, keep body
  3. Wrap in comment markers: <!-- skill:name -->...<!-- /skill:name -->
  4. Check byte budget — first skill always gets full body; subsequent must fit remaining budget
  5. If over budget, try summary fallback (see Budget Enforcement)
  6. Atomically claim skill in dedup system via tryClaimSessionKey() (O_EXCL)

Stage 6: Format Output

Assembles final JSON:

{
  "hookSpecificOutput": {
    "additionalContext": "<!-- skill:nextjs -->\n...body...\n<!-- /skill:nextjs -->\n<!-- skillInjection: {...metadata...} -->"
  }
}

The metadata comment includes matched skills, injected skills, match reasons, boost factors, and budget usage — useful for debugging.


UserPromptSubmit Pipeline

The UserPromptSubmit hook fires when the user types a prompt, before any tool calls. It uses prompt signal scoring instead of pattern matching.

Source: user-prompt-submit-skill-inject.mts

flowchart TD
    PROMPT["User prompt text"] --> LEN{"Length >= 10<br/>characters?"}
    LEN -->|No| SKIP["Skip (too short)"]
    LEN -->|Yes| NORM["Normalize prompt<br/>(lowercase, contractions, stem, whitespace)"]
    NORM --> SCORE["Score every skill's<br/>promptSignals against prompt"]
    SCORE --> INTENT["Classify troubleshooting<br/>intent (if any)"]
    INTENT --> COMPANION["Select investigation<br/>companion (if needed)"]
    COMPANION --> DEDUP["Filter seen skills"]
    DEDUP --> BUDGET["Budget enforcement<br/>(8KB / 2 skills)"]
    BUDGET --> OUTPUT["Format JSON output<br/>with additionalContext"]

Key differences from PreToolUse:

Parameter PreToolUse UserPromptSubmit
Budget 18 KB 8 KB
Max skills 5 2
Match method File/bash/import patterns Prompt signal scoring
Min input 10 characters

Prompt Signal Scoring

Source: hooks/src/prompt-patterns.mts

Each skill can define promptSignals in its frontmatter to declare which user prompts should trigger injection.

Normalization

Both the user's prompt and the signal terms undergo normalization before scoring:

  1. Lowercase"Deploy to Vercel""deploy to vercel"
  2. Contraction expansion"it's""it is", "don't""do not", "can't""cannot"
  3. Stemming"deploying""deploy", "configured""configur", "building""build"
  4. Whitespace collapse — multiple spaces/newlines → single space

Skill authors don't need to account for contractions or verb tenses in signal definitions.

Scoring Weights

flowchart TD
    N["Normalized prompt"] --> NONE{"noneOf<br/>matches?"}
    NONE -->|"Any term found"| SUPPRESS["score = -Infinity<br/>HARD SUPPRESS"]
    NONE -->|"No match"| PHRASES

    PHRASES["phrases check<br/>+6 per exact substring hit"] --> ALLOF
    ALLOF["allOf check<br/>+4 per group where ALL terms match"] --> ANYOF
    ANYOF["anyOf check<br/>+1 per hit, capped at +2 total"] --> TOTAL

    TOTAL{"total >= minScore?"}
    TOTAL -->|"Yes"| MATCHED["MATCHED — inject skill"]
    TOTAL -->|"No"| LEXICAL["Try lexical fallback"]

    style SUPPRESS fill:#ffcdd2
    style MATCHED fill:#c8e6c9
Signal Type Weight Behavior
phrases +6 per hit Exact substring match (case-insensitive, word-boundary aware)
allOf +4 per group All terms in the group must appear in the prompt
anyOf +1 per hit, capped at +2 Prevents low-signal flooding
noneOf -Infinity Hard suppress — skill is permanently excluded for this prompt
minScore threshold (default 6) Score must meet or exceed to qualify

Scoring Walkthrough

Prompt: "I want to add the ai sdk for streaming"

Skill signals:

promptSignals:
  phrases: ["ai sdk"]                    # "ai sdk" is a substring → +6
  allOf: [["streaming", "generation"]]   # Only "streaming" matched → +0
  anyOf: ["streaming"]                   # "streaming" present → +1
  minScore: 6
Component Matches? Score
phrases: "ai sdk" Yes (substring) +6
allOf: ["streaming", "generation"] Partial (1 of 2) +0
anyOf: "streaming" Yes +1
Total 7

7 >= minScore 6 → skill injects.

Another example — reaching threshold via allOf + anyOf only:

promptSignals:
  allOf: [["cron", "schedule"]]       # +4
  anyOf: ["vercel", "deploy", "prod"] # +1 each, capped at +2
  minScore: 6

Prompt: "I need to schedule a cron job on vercel for production"

  • allOf ["cron", "schedule"] both present → +4
  • anyOf "vercel" +1, "prod" (stemmed from "production") +1 → +2 (cap reached)
  • Total: 6 >= 6 → matched

Lexical Fallback

Source: prompt-patterns.mts:scorePromptWithLexical()

When exact scoring doesn't reach the threshold, a TF-IDF lexical index provides a fallback. The lexical score is boosted by 1.35x and compared against the exact score. The higher wins.

This catches prompts that are topically relevant but don't exactly hit configured phrases.

Troubleshooting Intent Classification

Source: user-prompt-submit-skill-inject.mts:classifyTroubleshootingIntent()

A regex classifier detects three troubleshooting intents:

Intent Pattern Examples Routed Skills
flow-verification "loads but", "submits but", "works locally but" verification
stuck-investigation "stuck", "frozen", "timed out", "not responding" investigation-mode
browser-only "blank page", "white screen", "console errors" agent-browser-verify + investigation-mode

Suppression: Test framework mentions (jest, vitest, playwright test) suppress all verification-family skills to avoid injecting browser guidance during unit testing.


Ranking & Boost Factors

Every matched skill goes through a ranking pipeline that applies priority adjustments in layers:

flowchart LR
    BASE["Base Priority<br/>SKILL.md frontmatter<br/>Range 48, default 5"]
    VJ["vercel.json Routing<br/>Relevant key: +10<br/>Irrelevant: 10"]
    PROF["Profiler Boost<br/>Likely skill: +5"]
    SETUP["Setup Mode<br/>bootstrap: +50"]
    TSX["TSX Review<br/>react-best-practices: +40"]
    DEV["Dev Server<br/>agent-browser-verify: +45"]

    BASE --> VJ --> PROF --> SETUP
    SETUP --> RANK["rankEntries()<br/>effectivePriority DESC<br/>Tiebreak: name ASC"]
    TSX -.-> RANK
    DEV -.-> RANK

    style BASE fill:#e1f5fe
    style VJ fill:#fff3e0
    style PROF fill:#e8f5e9
    style SETUP fill:#fce4ec
    style TSX fill:#f3e5f5
    style DEV fill:#ede7f6

Complete Boost Factor Table

Boost Value Condition Target Skill Source
Base priority 48 Always applied All skills SKILL.md frontmatter
Profiler +5 Skill is in VERCEL_PLUGIN_LIKELY_SKILLS Any detected skill session-start-profiler.mts
vercel.json relevant +10 Tool target is vercel.json and file keys match the skill routing-middleware, cron-jobs, vercel-functions, deployments-cicd vercel-config.mts
vercel.json irrelevant 10 Tool target is vercel.json but file keys don't match Skills that claim vercel.json in pathPatterns vercel-config.mts
Setup mode +50 3+ bootstrap hints detected; greenfield project bootstrap session-start-profiler.mts
TSX review +40 3+ .tsx edits (configurable via VERCEL_PLUGIN_REVIEW_THRESHOLD) react-best-practices pretooluse-skill-inject.mts
Dev server +45 Bash command matches dev server pattern (next dev, etc.) agent-browser-verify + companions pretooluse-skill-inject.mts

Priority formula:

effectivePriority = basePriority
                  + vercelJsonAdjustment   (±10 or 0)
                  + profilerBoost          (+5 or 0)
                  + setupModeBoost         (+50 or 0)
                  + specialTriggerBoost    (+40 or +45 or 0)

vercel.json Key-to-Skill Routing

Source: hooks/src/vercel-config.mts

When the tool target is vercel.json, the hook reads the file's top-level keys and maps them to skills:

vercel.json Keys Relevant Skill
redirects, rewrites, headers, cleanUrls, trailingSlash routing-middleware
crons cron-jobs
functions, regions vercel-functions
builds, buildCommand, installCommand, outputDirectory, framework, devCommand, ignoreCommand deployments-cicd

Skills relevant to the file's keys get +10; skills that claim vercel.json but aren't relevant get 10.

Profiler Detection

Source: hooks/src/session-start-profiler.mts

The session-start profiler scans the project and sets VERCEL_PLUGIN_LIKELY_SKILLS:

Detected Signal Skills Added
next.config.* file nextjs, turbopack
turbo.json file turborepo
vercel.json file vercel-cli, deployments-cicd, vercel-functions
middleware.ts|js file routing-middleware
components.json file shadcn
next in package.json deps nextjs
@vercel/blob|kv|postgres in deps vercel-storage
@vercel/flags in deps vercel-flags
crons key in vercel.json cron-jobs
...and many more See session-start-profiler.mts

Bootstrap/setup mode: When 3+ "bootstrap hints" are detected (e.g., .env.example, prisma/schema.prisma, auth dependencies, storage resource dependencies), VERCEL_PLUGIN_SETUP_MODE=1 is set, enabling the +50 boost for the bootstrap skill.


Dedup State Machine

The dedup system prevents re-injecting skills already delivered in the current session. It uses three independent state sources merged into a unified view:

stateDiagram-v2
    [*] --> Init: SessionStart sets<br/>VERCEL_PLUGIN_SEEN_SKILLS=""

    state "Three State Sources" as sources {
        EV: Env Var<br/>VERCEL_PLUGIN_SEEN_SKILLS<br/>(comma-delimited)
        CD: Claim Directory<br/>tmp/.../seen-skills.d/<br/>(one file per skill, O_EXCL)
        SF: Session File<br/>tmp/.../seen-skills.txt<br/>(comma-delimited snapshot)
    }

    Init --> sources: Hook invocation

    sources --> Merge: mergeSeenSkillStates()<br/>union all three sources

    Merge --> Check: Skill in merged set?
    Check --> Skip: Yes → already injected
    Check --> Claim: No → try atomic claim

    Claim --> Success: openSync(path, "wx")<br/>File created
    Claim --> Race: File already exists<br/>(concurrent hook won)

    Success --> Sync: syncSessionFileFromClaims()
    Race --> Skip

    state "Fallback Strategies" as fb {
        F1: file — atomic claims (primary)
        F2: env-var — no session ID
        F3: memory-only — single invocation
        F4: disabled — HOOK_DEDUP=off
    }

Key design choice: The atomic openSync(path, "wx") (O_EXCL) flag means if two hooks try to claim the same skill simultaneously, only one succeeds. This provides filesystem-level mutual exclusion.

Subagent isolation: Subagents (identified by scopeId) get their own dedup scope. The parent's env var is excluded from the subagent's merge, so subagents get fresh skill injection.

Cleanup: session-end-cleanup.mts deletes all temporary dedup files and claim directories when the session ends.


Budget Enforcement

Budget enforcement prevents the plugin from flooding Claude's context window.

PreToolUse Budget

Parameter Default Env Override
Byte budget 18,000 bytes (18 KB) VERCEL_PLUGIN_INJECTION_BUDGET
Max skills 5 — (constant MAX_SKILLS)

UserPromptSubmit Budget

Parameter Default Env Override
Byte budget 8,000 bytes (8 KB) VERCEL_PLUGIN_PROMPT_INJECTION_BUDGET
Max skills 2 — (constant MAX_SKILLS)

How Budget Is Applied

flowchart TD
    START["Ranked skills in priority order"] --> FIRST{"First skill?"}
    FIRST -->|Yes| ALWAYS["Always inject full body<br/>(even if over budget)"]
    FIRST -->|No| CHECK{"Body fits in<br/>remaining budget?"}

    CHECK -->|Yes| FULL["Inject full body"]
    CHECK -->|No| SUM{"Summary field<br/>exists + fits?"}

    SUM -->|Yes| SUMMARY["Inject summary only<br/><!-- skill:X mode:summary -->"]
    SUM -->|No| DROP["Drop skill<br/>(droppedByBudget)"]

    ALWAYS --> CAP{"Ceiling reached?<br/>(5 or 2 skills)"}
    FULL --> CAP
    SUMMARY --> CAP

    CAP -->|Yes| DONE["Stop — cap_exceeded"]
    CAP -->|No| NEXT["Next skill"] --> CHECK

Rules:

  • First skill always gets full body regardless of budget
  • Skills are measured as UTF-8 bytes after wrapping in comment markers
  • Summary fallback injects with a mode:summary marker
  • Skills that neither fit as full body nor summary are dropped

Special-Case Triggers

These operate alongside the normal matching pipeline with their own counter/dedup mechanisms.

TSX Review Trigger

Source: pretooluse-skill-inject.mts:checkTsxReviewTrigger()

Injects react-best-practices after repeated .tsx edits to catch React antipatterns early.

Parameter Default Env Override
Edit threshold 3 VERCEL_PLUGIN_REVIEW_THRESHOLD
Priority boost +40
Counter env VERCEL_PLUGIN_TSX_EDIT_COUNT

Behavior:

  1. Every Edit/Write on a .tsx file increments VERCEL_PLUGIN_TSX_EDIT_COUNT
  2. When count >= threshold, the trigger fires
  3. Counter resets after injection, allowing re-injection after another N edits
  4. Bypasses normal SEEN_SKILLS dedup — the counter is the sole gate

Dev Server Detection

Source: pretooluse-skill-inject.mts:checkDevServerVerify()

When Claude runs a dev server command, agent-browser-verify injects to encourage browser-based verification.

Detected dev server patterns:

next dev, npm run dev, pnpm dev, bun dev, bun run dev,
yarn dev, vite dev, vite, nuxt dev, vercel dev, astro dev
Parameter Value
Priority boost +45
Max iterations 2 per session
Loop guard env VERCEL_PLUGIN_DEV_VERIFY_COUNT
Companion skills verification (co-injected as summary)

Graceful degradation: If agent-browser is not installed (VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE=0), the hook injects an unavailability notice suggesting the user install it.

Vercel Env Help

Source: pretooluse-skill-inject.mts

One-time injection of a quick-reference guide when Claude runs vercel env add|update|pull. Uses standard dedup with key vercel-env-help.

Investigation Companion Selection

Source: user-prompt-submit-skill-inject.mts:selectInvestigationCompanion()

When investigation-mode is selected via prompt signals, the second skill slot is reserved for the best companion from a prioritized list:

  1. workflow (highest priority)
  2. agent-browser-verify
  3. vercel-cli

The companion must have independently scored above its minScore.


User Stories

User Story 1: TSX Edit Trigger

Scenario: Sarah is building a dashboard. She's been editing React components and is on her 3rd .tsx file edit.

What happens:

sequenceDiagram
    participant S as Sarah
    participant CC as Claude Code
    participant Hook as PreToolUse Hook

    S->>CC: "Add a useEffect to fetch data"
    CC->>Hook: Edit tool on dashboard.tsx
    Note over Hook: TSX_EDIT_COUNT: 1 → 2
    Hook-->>CC: (no injection, count < 3)

    S->>CC: "Now add the loading state"
    CC->>Hook: Edit tool on dashboard.tsx
    Note over Hook: TSX_EDIT_COUNT: 2 → 3
    Note over Hook: Threshold reached!
    Note over Hook: Inject react-best-practices (+40 boost)
    Hook-->>CC: additionalContext with React best practices

    Note over CC: Claude now has guidance on:<br/>- Hook dependencies<br/>- Memoization<br/>- Error boundaries<br/>- "use client" directive

    S->>CC: "Extract this into a custom hook"
    CC->>Hook: Edit tool on useData.tsx
    Note over Hook: TSX_EDIT_COUNT: 0 → 1 (reset after injection)
    Hook-->>CC: (no injection, fresh counter)

Why it matters: After several TSX edits, Claude accumulates context about what the developer is building. The react-best-practices skill arrives at the right moment — when Claude has enough context to apply the guidance meaningfully, and before the code grows too large to refactor easily.

Key details:

  • The counter increments on any .tsx file edit (Write or Edit tool)
  • After injection, the counter resets to 0, not to 1
  • The trigger bypasses normal dedup — it can fire multiple times per session
  • The +40 boost ensures react-best-practices outranks almost any other skill

User Story 2: Dev Server Detection

Scenario: Marcus just finished implementing a feature and asks Claude to start the dev server so he can test it.

What happens:

sequenceDiagram
    participant M as Marcus
    participant CC as Claude Code
    participant Hook as PreToolUse Hook
    participant Prof as Session Profiler

    Note over Prof: Session start: detected agent-browser CLI<br/>Set AGENT_BROWSER_AVAILABLE=1

    M->>CC: "Start the dev server"
    CC->>Hook: Bash tool: "npm run dev"
    Note over Hook: Dev server pattern matched!<br/>agent-browser available ✓<br/>Iteration count: 0 < 2

    Note over Hook: Inject agent-browser-verify (+45)<br/>+ verification companion (summary)
    Hook-->>CC: additionalContext with browser verification guide

    Note over CC: Claude now knows to:<br/>1. Wait for server to be ready<br/>2. Open browser to verify<br/>3. Check for console errors<br/>4. Take a screenshot

    M->>CC: "Looks broken, restart the dev server"
    CC->>Hook: Bash tool: "npm run dev"
    Note over Hook: Iteration count: 1 < 2
    Hook-->>CC: Browser verification injected again

    M->>CC: "One more restart please"
    CC->>Hook: Bash tool: "npm run dev"
    Note over Hook: Iteration count: 2 >= 2<br/>Loop guard hit!
    Hook-->>CC: (no injection — prevents infinite loops)

Why it matters: When a developer starts a dev server, they expect to see their changes in a browser. The plugin nudges Claude to verify using browser automation rather than just assuming the server started correctly.

Key details:

  • The profiler checks at session start whether agent-browser CLI is on PATH
  • If not installed, the hook injects an unavailability notice instead (suggesting installation)
  • The loop guard (max 2 iterations) prevents the skill from being injected on every dev server restart
  • The verification skill is co-injected as a summary-only companion

User Story 3: Prompt Signal Matching

Scenario: Jess is building a Next.js app and types "my deploy keeps failing with a timeout error" into Claude Code.

What happens:

sequenceDiagram
    participant J as Jess
    participant CC as Claude Code
    participant Hook as UserPromptSubmit Hook

    J->>CC: "my deploy keeps failing with a timeout error"

    Note over Hook: Step 1: Normalize prompt
    Note over Hook: "my deploy keep fail with a timeout error"<br/>(lowercase + stem "keeps"→"keep", "failing"→"fail")

    Note over Hook: Step 2: Score all skills with promptSignals

    rect rgb(255, 243, 224)
        Note over Hook: investigation-mode scoring:<br/>phrase "timeout" → not exact (no "timeout error" phrase)<br/>allOf ["timeout", "api"] → "timeout" ✓, "api" ✗ → +0<br/>anyOf ["timeout", "stuck", "debug"] → "timeout" +1 → +1<br/>Score: 1 < minScore 4 → NOT MATCHED
    end

    rect rgb(232, 245, 233)
        Note over Hook: deployments-cicd scoring:<br/>phrase "deploy" → +6<br/>allOf ["deploy", "fail"] → both present → +4<br/>anyOf ["timeout", "error"] → +1, +1 → +2 (capped)<br/>Score: 12 >= minScore 6 → MATCHED ✓
    end

    rect rgb(227, 242, 253)
        Note over Hook: vercel-functions scoring:<br/>phrase "timeout" → +6 (if configured)<br/>Score: 6 >= minScore 6 → MATCHED ✓
    end

    Note over Hook: Step 3: Rank by score DESC
    Note over Hook: deployments-cicd (12) > vercel-functions (6)
    Note over Hook: Step 4: Budget check (8KB, max 2 skills)

    Hook-->>CC: additionalContext with deployment + functions guidance

    Note over CC: Claude now has guidance on:<br/>- Deployment debugging steps<br/>- Function timeout configuration<br/>- Vercel build logs analysis

Why it matters: The prompt signal system catches user intent even when they don't mention specific technologies. The scoring formula ensures the most relevant skill wins — deployments-cicd scores higher because it matches on both the phrase "deploy" and the allOf group ["deploy", "fail"].

Key details:

  • Stemming converts "failing" → "fail" and "keeps" → "keep", making signals match naturally
  • The noneOf mechanism ensures skills aren't injected for irrelevant contexts (e.g., investigation-mode has noneOf: ["css stuck", "sticky position"])
  • The 8KB budget and 2-skill cap keep prompt injection lean since it's speculative
  • If both investigation-mode and a companion were matched, investigation companion selection would kick in

PostToolUse Validation

Source: hooks/src/posttooluse-validate.mts

After Claude writes or edits a file, the PostToolUse hook runs validation rules from matched skills.

flowchart TD
    WRITE["Claude writes/edits file"] --> MATCH["Match file path → skills<br/>(using pathPatterns)"]
    MATCH --> LOOP["For each matched skill"]
    LOOP --> RULE["For each validate rule"]
    RULE --> SKIP{"skipIfFileContains<br/>matches?"}
    SKIP -->|Yes| NEXT["Skip rule"]
    SKIP -->|No| TEST{"pattern matches<br/>any line?"}
    TEST -->|Yes| REPORT["Report violation<br/>(severity + message)"]
    TEST -->|No| NEXT
    NEXT --> RULE
    REPORT --> LOOP

Validation rule fields:

Field Type Required Description
pattern string (regex) Yes Pattern to search for in file content
message string Yes Actionable fix instruction for Claude
severity "error" or "warn" Yes error = must fix; warn = advisory
skipIfFileContains string (regex) No Skip rule if file matches this pattern

Example — Next.js async cookies rule:

validate:
  - pattern: (?<!await )\bcookies\(\s*\)
    message: 'cookies() is async in Next.js 16 — add await'
    severity: error
    skipIfFileContains: "^['\"]use client['\"]"

This catches cookies() calls without await, but skips client components (which can't call cookies() anyway).

Dedup: Validation uses MD5 content hashing to avoid re-validating the same file content. The hash is tracked in VERCEL_PLUGIN_VALIDATED_FILES.


Environment Variables Reference

Variable Default Set By Description
VERCEL_PLUGIN_SEEN_SKILLS "" session-start Comma-delimited already-injected skills
VERCEL_PLUGIN_LIKELY_SKILLS "" profiler Comma-delimited profiler-detected skills (+5 boost)
VERCEL_PLUGIN_GREENFIELD profiler "true" if project is empty
VERCEL_PLUGIN_SETUP_MODE profiler "1" if 3+ bootstrap hints detected (+50 boost)
VERCEL_PLUGIN_BOOTSTRAP_HINTS profiler Comma-delimited bootstrap signals found
VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE "0" profiler "1" if agent-browser CLI is on PATH
VERCEL_PLUGIN_TSX_EDIT_COUNT "0" pretooluse Current .tsx edit count
VERCEL_PLUGIN_DEV_VERIFY_COUNT "0" pretooluse Dev server injection iteration count
VERCEL_PLUGIN_VALIDATED_FILES posttooluse Content hashes of validated files
VERCEL_PLUGIN_INJECTION_BUDGET 18000 PreToolUse byte budget
VERCEL_PLUGIN_PROMPT_INJECTION_BUDGET 8000 UserPromptSubmit byte budget
VERCEL_PLUGIN_REVIEW_THRESHOLD 3 TSX edits before react-best-practices triggers
VERCEL_PLUGIN_LOG_LEVEL off off / summary / debug / trace
VERCEL_PLUGIN_HOOK_DEDUP off to disable dedup entirely
VERCEL_PLUGIN_AUDIT_LOG_FILE Audit log path or off