Files
vercel__vercel-plugin/docs/hook-lifecycle.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

32 KiB

Hook Lifecycle Deep Dive

This document covers every hook entry point in hooks/hooks.json, organized by lifecycle phase. Each section includes input/output contracts, sequence diagrams, and implementation details.


Table of Contents

  1. Lifecycle Overview
  2. SessionStart Phase
  3. PreToolUse Phase
  4. UserPromptSubmit Phase
  5. PostToolUse Phase
  6. SubagentStart Phase
  7. SubagentStop Phase
  8. SessionEnd Phase
  9. Hook I/O Contract
  10. Custom YAML Parser Semantics
  11. Environment Variables Reference

Lifecycle Overview

Every hook fires at a specific point in Claude Code's execution cycle. The following diagram shows the complete lifecycle from session start to session end, including all 12 hook entry points.

sequenceDiagram
    participant CC as Claude Code
    participant SS as SessionStart Hooks
    participant Agent as Agent (LLM)
    participant PTU as PreToolUse Hooks
    participant UPS as UserPromptSubmit Hook
    participant PostTU as PostToolUse Hooks
    participant SA as Subagent Hooks
    participant SE as SessionEnd Hook

    Note over CC,SE: Session Lifecycle

    CC->>SS: startup | resume | clear | compact
    activate SS
    SS-->>CC: Initialize dedup, profile project, inject vercel.md
    deactivate SS

    loop Every user prompt
        CC->>UPS: User types a prompt
        activate UPS
        UPS-->>CC: Score prompt signals -> inject 0-2 skills
        deactivate UPS

        loop Every tool call
            CC->>PTU: Agent calls Read/Edit/Write/Bash/Agent
            activate PTU
            PTU-->>CC: Match patterns -> inject 0-3 skills
            deactivate PTU

            Agent->>Agent: Tool executes

            CC->>PostTU: Tool completes (Write/Edit/Bash)
            activate PostTU
            PostTU-->>CC: Validate files / observe verification / fix fonts
            deactivate PostTU
        end
    end

    opt Agent spawns subagent
        CC->>SA: SubagentStart
        activate SA
        SA-->>CC: Bootstrap context (1-8KB by agent type)
        deactivate SA

        Note over SA: Subagent works...

        CC->>SA: SubagentStop
        activate SA
        SA-->>CC: Write ledger, sync dedup
        deactivate SA
    end

    CC->>SE: Session ends
    activate SE
    SE-->>CC: Delete all temp files
    deactivate SE

SessionStart Phase

These hooks fire once when a session begins, resumes, is cleared, or compacted. They set up the environment for all subsequent hooks.

Matcher: startup|resume|clear|compact

Execution order: Hooks run in the order listed in hooks.json — seen-skills first, then profiler, then inject-claude-md.


1. session-start-seen-skills

Source: hooks/src/session-start-seen-skills.mts (17 lines) Timeout: None Output: None (side-effect only)

Purpose

Initializes the dedup state by writing VERCEL_PLUGIN_SEEN_SKILLS="" to CLAUDE_ENV_FILE. This ensures the PreToolUse and UserPromptSubmit hooks start with a blank slate for skill dedup tracking.

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as session-start-seen-skills
    participant Env as CLAUDE_ENV_FILE

    CC->>Hook: SessionStart event (stdin: JSON)
    Hook->>Env: appendFileSync('export VERCEL_PLUGIN_SEEN_SKILLS=""')
    Hook-->>CC: exit 0 (no stdout)

Implementation Details

  • Reads CLAUDE_ENV_FILE from environment (required — requireEnvFile() exits if missing)
  • Appends a single export line — does not overwrite existing content
  • Failures are silently ignored (non-critical)
  • This must run before the profiler to ensure the env var exists when the profiler writes LIKELY_SKILLS

2. session-start-profiler

Source: hooks/src/session-start-profiler.mts (620 lines) Timeout: None Output: stdout text (CLI status messages), env var side-effects

Purpose

Scans the project's package.json, config files, directory structure, and Vercel CLI version to:

  1. Determine which skills are likely relevant (VERCEL_PLUGIN_LIKELY_SKILLS)
  2. Detect bootstrap/setup signals (VERCEL_PLUGIN_BOOTSTRAP_HINTS, VERCEL_PLUGIN_SETUP_MODE)
  3. Detect greenfield (empty) projects (VERCEL_PLUGIN_GREENFIELD)
  4. Check if agent-browser CLI is available (VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE)
  5. Report Vercel CLI installation and update status

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as session-start-profiler
    participant FS as File System
    participant Env as CLAUDE_ENV_FILE
    participant Cache as Profile Cache (tmpdir)

    CC->>Hook: SessionStart event (stdin: { session_id })
    Hook->>FS: Check greenfield (readdirSync)
    Hook->>FS: Scan FILE_MARKERS (next.config.*, vercel.json, etc.)
    Hook->>FS: Read package.json -> match PACKAGE_MARKERS
    Hook->>FS: Read vercel.json -> check crons, rewrites, functions
    Hook->>Hook: Detect bootstrap signals (env templates, prisma, drizzle, auth)
    Hook->>Hook: Check Vercel CLI version (vercel --version + npm view)
    Hook->>Hook: Check agent-browser on PATH
    Hook->>Env: Write VERCEL_PLUGIN_LIKELY_SKILLS
    Hook->>Env: Write VERCEL_PLUGIN_GREENFIELD (if empty)
    Hook->>Env: Write VERCEL_PLUGIN_SETUP_MODE (if hints >= 3)
    Hook->>Env: Write VERCEL_PLUGIN_BOOTSTRAP_HINTS
    Hook->>Env: Write VERCEL_PLUGIN_RESOURCE_HINTS
    Hook->>Env: Write VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE
    Hook->>Cache: Write profile.json (for subagent bootstrap)
    Hook-->>CC: stdout: CLI status messages (if outdated/missing)

File Markers

The profiler checks for these files to determine likely skills:

File Skills Detected
next.config.{js,mjs,ts,mts} nextjs, turbopack
turbo.json turborepo
vercel.json vercel-cli, deployments-cicd, vercel-functions
.mcp.json vercel-api
middleware.{ts,js} routing-middleware
components.json shadcn
.env.local env-vars
pnpm-workspace.yaml turborepo

Package Markers

Dependencies in package.json map to skills:

Package Skills
next nextjs
ai, @ai-sdk/* ai-sdk, ai-elements, ai-gateway
@vercel/blob, @vercel/kv, @vercel/postgres, @vercel/edge-config vercel-storage
@vercel/analytics, @vercel/speed-insights observability
@vercel/flags vercel-flags
@vercel/workflow workflow
@vercel/queue vercel-queues
turbo turborepo
@repo/*, @t3-oss/env-nextjs next-forge

Bootstrap Signal Detection

The profiler detects setup/bootstrap signals that trigger VERCEL_PLUGIN_SETUP_MODE when 3 or more hints are found:

  • Env templates: .env.example, .env.sample, .env.template
  • README: Any file starting with readme
  • Database: drizzle.config.*, prisma/schema.prisma, db:push/db:seed scripts
  • Auth: next-auth, @auth/core, better-auth dependencies
  • Resources: @neondatabase/serverless, drizzle-orm, @upstash/redis

Greenfield Detection

A project is greenfield if:

  • Every top-level entry is a dot-directory (.git, .claude)
  • No dot-files exist (.env.local, .mcp.json indicate real config)

Greenfield projects get default skills: nextjs, ai-sdk, vercel-cli, env-vars.


3. inject-claude-md

Source: hooks/src/inject-claude-md.mts (33 lines) Timeout: None Output: stdout text (vercel.md content as additionalContext)

Purpose

Outputs the vercel.md ecosystem graph (~52KB) as additionalContext. This gives the agent a map of the entire Vercel ecosystem before any specific skills fire. If the project is greenfield, it also appends execution mode instructions.

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as inject-claude-md
    participant FS as File System

    CC->>Hook: SessionStart event
    Hook->>FS: Read vercel.md from plugin root
    alt Greenfield project
        Hook->>Hook: Append greenfield execution instructions
        Note over Hook: "Skip planning, choose defaults, start executing"
    end
    Hook-->>CC: stdout: vercel.md content (~52KB)

PreToolUse Phase

These hooks fire before a tool call executes. They can inject additional context or observe the pending action.


4. pretooluse-skill-inject

Source: hooks/src/pretooluse-skill-inject.mts (~1300 lines) Matcher: Read|Edit|Write|Bash Timeout: 5 seconds Output: JSON with additionalContext

Purpose

The main injection engine. When the agent calls Read, Edit, Write, or Bash, this hook:

  1. Parses the tool input (file path or bash command)
  2. Matches against all skills' pathPatterns, bashPatterns, and importPatterns
  3. Applies priority boosters (profiler, vercel.json, setup mode)
  4. Deduplicates against already-injected skills
  5. Injects up to 3 skills within an 18KB byte budget

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as pretooluse-skill-inject
    participant Manifest as skill-manifest.json
    participant Dedup as Dedup State
    participant Skills as SKILL.md files

    CC->>Hook: PreToolUse (stdin: { tool_name, tool_input, session_id })
    Hook->>Hook: parseInput -> extract file path or bash command
    Hook->>Manifest: Load skill map (prefer manifest over scanning)
    Hook->>Hook: compileSkillPatterns -> create regex matchers
    Hook->>Hook: matchPathWithReason / matchBashWithReason / matchImportWithReason
    Hook->>Hook: Apply vercel.json routing (+-10)
    Hook->>Hook: Apply profiler boost (+5 for LIKELY_SKILLS)
    Hook->>Hook: Apply setup mode boost (+50 if SETUP_MODE=1)
    Hook->>Hook: Check TSX review trigger (+40 after N edits)
    Hook->>Hook: Check dev server detection
    Hook->>Hook: rankEntries -> sort by final priority DESC
    Hook->>Dedup: mergeSeenSkillStates (env + file + claims)
    Hook->>Dedup: Filter already-seen skills
    loop For each ranked skill (up to 3, within 18KB)
        Hook->>Skills: Read SKILL.md body
        alt Body fits budget
            Hook->>Hook: Add full body to parts
        else Over budget
            Hook->>Hook: Add summary fallback
        end
        Hook->>Dedup: Atomic claim + update env var
    end
    Hook-->>CC: JSON { hookSpecificOutput: { additionalContext } }

Pipeline Stages

The hook is organized as a testable pipeline:

parseInput -> loadSkills -> matchSkills -> deduplicateSkills -> injectSkills -> formatOutput

Special Triggers

Trigger Condition Effect
TSX review After VERCEL_PLUGIN_REVIEW_THRESHOLD (default 3) .tsx edits Injects react-best-practices with +40 priority boost
Dev server detection Bash command matches next dev, npm run dev, etc. Boosts agent-browser-verify
Vercel env help First vercel env command One-time injection of env-vars guidance
Setup mode VERCEL_PLUGIN_SETUP_MODE=1 +50 priority boost for matched skills

Input Schema

{
  "tool_name": "Read|Edit|Write|Bash",
  "tool_input": {
    "file_path": "app/page.tsx",
    "command": "vercel deploy --prod"
  },
  "session_id": "abc-123",
  "cwd": "/Users/dev/my-app"
}

Output Schema

{
  "hookSpecificOutput": {
    "additionalContext": "<!-- skillInjection: {...} -->\n[vercel-plugin] Best practices...\n\n<!-- skill:nextjs -->\n..."
  }
}

5. pretooluse-subagent-spawn-observe

Source: hooks/src/pretooluse-subagent-spawn-observe.mts (141 lines) Matcher: Agent Timeout: 5 seconds Output: {} (observer only)

Purpose

Fires when the agent spawns a subagent. Records the pending launch metadata (description, prompt, subagent_type) in tmpdir so the subagent-start-bootstrap hook can correlate the launch with the right skill context.

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as pretooluse-subagent-spawn-observe
    participant State as Pending Launch State (tmpdir)

    CC->>Hook: PreToolUse (stdin: { tool_name: "Agent", tool_input: {...} })
    Hook->>Hook: parseInput -> validate Agent tool, extract session_id
    Hook->>Hook: buildPendingLaunchRecord(toolInput, timestamp)
    Hook->>State: appendPendingLaunch(sessionId, payload)
    Note over State: JSONL in <tmpdir>/vercel-plugin-<sid>-pending-launches/
    Hook-->>CC: "{}" (no mutation)

What Gets Recorded

{
  "description": "Research authentication patterns",
  "prompt": "Find how auth is implemented in this codebase",
  "subagent_type": "Explore",
  "createdAt": 1710000000000,
  "name": "auth-researcher"
}

UserPromptSubmit Phase

This hook fires when the user submits a prompt, before the agent processes it.


6. user-prompt-submit-skill-inject

Source: hooks/src/user-prompt-submit-skill-inject.mts (703 lines) Matcher: (all prompts) Timeout: 5 seconds Output: JSON with additionalContext

Purpose

Scores the user's prompt text against promptSignals defined in skill frontmatter. Injects up to 2 skills within an 8KB budget. Also handles troubleshooting intent routing and investigation companion selection.

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as user-prompt-submit
    participant Skills as Skill Map
    participant Dedup as Dedup State
    participant Analysis as Prompt Analysis

    CC->>Hook: UserPromptSubmit (stdin: { prompt, session_id })
    Hook->>Hook: parsePromptInput -> validate length >= 10 chars
    Hook->>Hook: normalizePromptText -> lowercase, expand contractions
    Hook->>Skills: loadSkills -> build skill map
    Hook->>Analysis: analyzePrompt -> score all skills with promptSignals

    loop For each skill with promptSignals
        Analysis->>Analysis: Score phrases (+6 each)
        Analysis->>Analysis: Score allOf groups (+4 per match)
        Analysis->>Analysis: Score anyOf terms (+1 each, cap +2)
        Analysis->>Analysis: Check noneOf (-Infinity if matched)
        Analysis->>Analysis: Compare score vs minScore (default 6)
    end

    Hook->>Hook: classifyTroubleshootingIntent
    alt Investigation mode triggered
        Hook->>Hook: selectInvestigationCompanion
        Note over Hook: Pick best from: workflow, agent-browser-verify, vercel-cli
    end
    alt Test framework mentioned
        Hook->>Hook: Suppress verification-family skills
    end

    Hook->>Dedup: Filter already-seen skills
    Hook->>Hook: Cap at 2 skills, enforce 8KB budget

    loop For each selected skill
        Hook->>Skills: Read SKILL.md body
        Hook->>Dedup: Atomic claim + sync
    end

    Hook-->>CC: JSON { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext } }

Scoring Example

Given a skill with:

promptSignals:
  phrases: ["deploy to preview"]  # +6
  allOf: [["deploy", "branch"]]   # +4
  anyOf: ["ci", "github"]         # +1 each, cap +2
  noneOf: ["rollback"]
  minScore: 6
  • Prompt "how do I deploy to preview?" -> phrase match (+6) -> score 6 >= minScore 6 -> matched
  • Prompt "deploy my branch to CI" -> allOf (+4) + anyOf "ci" (+1) -> score 5 < minScore 6 -> not matched
  • Prompt "rollback the deploy" -> noneOf "rollback" -> score -Infinity -> suppressed

Investigation Companion Selection

When investigation-mode is selected, the hook picks the best companion skill:

Priority Companion When Selected
1st workflow Best score among companions
2nd agent-browser-verify If workflow doesn't match
3rd vercel-cli Fallback companion

PostToolUse Phase

These hooks fire after a tool call completes. They observe results, validate outputs, or apply fixes.


7. posttooluse-shadcn-font-fix

Source: hooks/posttooluse-shadcn-font-fix.mjs (standalone, no .mts source) Matcher: Bash Timeout: 5 seconds Output: JSON with additionalContext (fix instructions)

Purpose

After a Bash command completes, detects and fixes shadcn font loading issues. This is a standalone hook with no TypeScript source — it's a simple pattern-match-and-fix.


8. posttooluse-verification-observe

Source: hooks/src/posttooluse-verification-observe.mts (285 lines) Matcher: Bash Timeout: 5 seconds Output: {} (observer only — emits structured log events)

Purpose

After a Bash command completes, classifies the command into a verification boundary type and emits structured log events. This powers the verification pipeline that tracks whether the agent is testing at all system boundaries.

Boundary Classification

Boundary Pattern Examples Label
uiRender open, screenshot, playwright, puppeteer Browser/UI interaction
clientRequest curl, wget, fetch(, httpie HTTP client requests
serverHandler tail -f *.log, vercel logs, port inspection Server/log inspection
environment printenv, vercel env, cat .env Environment reads

Story Inference

The hook infers the target route from two sources (in priority order):

  1. VERCEL_PLUGIN_RECENT_EDITS — file paths recently edited, e.g. app/settings/page.tsx -> /settings
  2. URL patterns in the command itself, e.g. curl http://localhost:3000/api/data -> /api/data

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as posttooluse-verification-observe
    participant Log as Structured Logger

    CC->>Hook: PostToolUse (stdin: { tool_name: "Bash", tool_input: { command } })
    Hook->>Hook: parseInput -> extract command
    Hook->>Hook: classifyBoundary(command)
    alt Boundary matched
        Hook->>Hook: inferRoute(command, RECENT_EDITS)
        Hook->>Log: Emit verification.boundary_observed event
        Note over Log: { boundary, verificationId, command, inferredRoute }
    end
    Hook-->>CC: "{}" (observer only)

9. posttooluse-validate

Source: hooks/src/posttooluse-validate.mts (550 lines) Matcher: Write|Edit Timeout: 5 seconds Output: JSON with additionalContext (validation violations)

Purpose

After a Write or Edit, matches the target file against skills and runs any validate rules defined in skill frontmatter. Returns fix instructions if validation fails.

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as posttooluse-validate
    participant Skills as Skill Map
    participant FS as File System
    participant Dedup as Validation Dedup

    CC->>Hook: PostToolUse (stdin: { tool_name: "Write", tool_input: { file_path } })
    Hook->>Hook: parseInput -> extract file path
    Hook->>FS: Read file content from disk
    Hook->>Dedup: Check file+hash already validated?
    alt Already validated (same content)
        Hook-->>CC: "{}" (skip)
    end
    Hook->>Skills: loadValidateRules -> filter skills with validate: rules
    Hook->>Hook: matchFileToSkills -> match by path globs + import patterns
    loop For each matched skill's validate rules
        Hook->>Hook: Check skipIfFileContains regex
        Hook->>Hook: Run pattern regex against each line
        alt Pattern matches
            Hook->>Hook: Record violation (line, message, severity)
        end
    end
    Hook->>Dedup: Mark file+hash as validated
    Hook-->>CC: JSON with violations or "{}"

Validation Rule Format

validate:
  - pattern: "executeRaw\\("
    message: "Use $queryRaw for type safety instead of executeRaw"
    severity: "error"
    skipIfFileContains: "\\$queryRaw"
  • pattern: Regex matched against each line of the file
  • message: Error description returned to the agent
  • severity: error (mandatory fix) or warn (suggestion)
  • skipIfFileContains: If this regex matches anywhere in the file, skip this rule

Validation Dedup

Tracks file_path:content_hash pairs in VERCEL_PLUGIN_VALIDATED_FILES to avoid re-validating unchanged files. Uses MD5 hash (first 12 hex chars) for fast comparison.


SubagentStart Phase

This hook fires when any subagent starts.


10. subagent-start-bootstrap

Source: hooks/src/subagent-start-bootstrap.mts (427 lines) Matcher: .+ (any subagent) Timeout: 5 seconds Output: JSON with additionalContext

Purpose

When any subagent starts, bootstraps it with relevant skill context. The context size is tailored to the agent type:

Agent Type Budget Content Strategy
Explore 1KB (minimal) Project profile line + skill name list
Plan 3KB (light) Profile + skill summaries + deployment constraints
general-purpose 8KB (standard) Profile + full skill bodies (with summary fallback)
Other/custom 8KB (standard) Same as general-purpose

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as subagent-start-bootstrap
    participant Cache as Profile Cache
    participant State as Pending Launch State
    participant Skills as Skill Map
    participant Dedup as Dedup Claims

    CC->>Hook: SubagentStart (stdin: { session_id, agent_id, agent_type })
    Hook->>Cache: Read profiler cache (profile.json)
    alt Cache hit
        Hook->>Hook: Use cached likelySkills
    else Cache miss
        Hook->>Hook: Fallback to VERCEL_PLUGIN_LIKELY_SKILLS env var
    end
    Hook->>State: claimPendingLaunch(sessionId, agentType)
    alt Pending launch found
        Hook->>Hook: Match prompt text against skill signals
        Hook->>Hook: Merge prompt-matched skills with profiler skills
    end
    Hook->>Hook: resolveBudgetCategory(agentType)
    alt Minimal (Explore)
        Hook->>Hook: buildMinimalContext (profile + skill names)
    else Light (Plan)
        Hook->>Skills: Load skill summaries within 3KB
        Hook->>Hook: buildLightContext (profile + summaries + constraints)
    else Standard (general-purpose)
        Hook->>Skills: Load full SKILL.md bodies within 8KB
        Hook->>Hook: buildStandardContext (profile + full bodies)
    end
    Hook->>Dedup: Claim injected skills (scoped by agentId)
    Hook-->>CC: JSON { hookSpecificOutput: { hookEventName: "SubagentStart", additionalContext } }

Pending Launch Correlation

The hook reads the pending launch directory written by pretooluse-subagent-spawn-observe to extract the subagent's description and prompt. It then runs prompt signal matching against this text to determine additional relevant skills beyond what the profiler detected.


SubagentStop Phase

This hook fires when any subagent stops.


11. subagent-stop-sync

Source: hooks/src/subagent-stop-sync.mts (141 lines) Matcher: .+ (any subagent) Timeout: 5 seconds Output: None (side-effect only)

Purpose

When any subagent stops, writes a JSONL ledger entry for observability and counts the skills injected for that agent.

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as subagent-stop-sync
    participant Ledger as Ledger File (JSONL)
    participant Dedup as Dedup Claims

    CC->>Hook: SubagentStop (stdin: { session_id, agent_id, agent_type, agent_transcript_path })
    Hook->>Ledger: Append JSONL record
    Note over Ledger: <tmpdir>/vercel-plugin-<sid>-subagent-ledger.jsonl
    Hook->>Dedup: Count skills injected for this agent (scoped claims)
    Hook->>Hook: Log summary (agent_id, agent_type, skills_injected)
    Hook-->>CC: exit 0 (no stdout)

Ledger Entry Format

{
  "timestamp": "2026-03-10T12:00:00.000Z",
  "session_id": "abc-123",
  "agent_id": "agent-456",
  "agent_type": "Explore",
  "agent_transcript_path": "/path/to/transcript"
}

SessionEnd Phase

This hook fires when the session ends.


12. session-end-cleanup

Source: hooks/src/session-end-cleanup.mts (81 lines) Matcher: None (fires on all session ends) Timeout: None Output: None (side-effect only)

Purpose

Best-effort cleanup of all session-scoped temporary files. Always exits successfully, even if cleanup fails.

What Gets Cleaned Up

Path Pattern Type Contents
<tmpdir>/vercel-plugin-<sid>-seen-skills.d/ Directory Atomic skill claim files
<tmpdir>/vercel-plugin-<sid>-seen-skills.txt File Comma-delimited seen skills
<tmpdir>/vercel-plugin-<sid>-pending-launches/ Directory Subagent pending launch records
<tmpdir>/vercel-plugin-<sid>-subagent-ledger.jsonl File Subagent lifecycle ledger
<tmpdir>/vercel-plugin-<sid>-profile.json File Profiler cache
<tmpdir>/vercel-plugin-<sid>-validated-files.txt File Validation dedup state

Sequence

sequenceDiagram
    participant CC as Claude Code
    participant Hook as session-end-cleanup
    participant FS as File System (tmpdir)

    CC->>Hook: SessionEnd (stdin: { session_id })
    Hook->>Hook: Parse session_id from stdin
    Hook->>Hook: Hash session_id if non-alphanumeric
    Hook->>FS: readdirSync(tmpdir) -> filter by prefix
    loop For each matching entry
        alt Entry ends with .d or -pending-launches
            Hook->>FS: rmSync(path, { recursive: true })
        else Regular file
            Hook->>FS: unlinkSync(path)
        end
    end
    Hook-->>CC: exit 0 (always succeeds)

Hook I/O Contract

All hooks follow the same I/O contract defined by SyncHookJSONOutput from @anthropic-ai/claude-agent-sdk:

Input (stdin)

{
  "tool_name": "Read",
  "tool_input": { "file_path": "app/page.tsx" },
  "session_id": "abc-123",
  "cwd": "/Users/dev/my-app",
  "hook_event_name": "PreToolUse"
}

For UserPromptSubmit:

{
  "prompt": "How do I deploy to preview?",
  "session_id": "abc-123",
  "cwd": "/Users/dev/my-app",
  "hook_event_name": "UserPromptSubmit"
}

For SubagentStart / SubagentStop:

{
  "session_id": "abc-123",
  "cwd": "/Users/dev/my-app",
  "agent_id": "agent-456",
  "agent_type": "Explore",
  "hook_event_name": "SubagentStart"
}

Output (stdout)

Hooks that inject context return:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "<!-- skill:nextjs -->\n..."
  }
}

Observer-only hooks and hooks with no matches return:

{}

Error Handling

All hooks follow defensive patterns:

  • Catch all errors and log to stderr
  • Always write valid JSON to stdout (at minimum {})
  • Never crash the Claude Code session — graceful degradation is preferred
  • Timeouts (5s) kill the hook process; Claude Code continues without the hook's output

Custom YAML Parser Semantics

The plugin uses parseSimpleYaml (in hooks/src/skill-map-frontmatter.mts), a custom inline YAML parser purpose-built for skill frontmatter. It is not js-yaml.

Why a Custom Parser?

Skill frontmatter values are always used as strings for pattern matching. The standard YAML spec converts values like null, true, and false to their JavaScript equivalents, which would break pattern matching.

Behavioral Differences

Input Standard YAML (js-yaml) vercel-plugin parser Rationale
Bare null JavaScript null String "null" Patterns should always be strings
Bare true JavaScript true String "true" No type coercion
Bare false JavaScript false String "false" No type coercion
Unclosed [items Parse error (throws) Scalar string "[items" Graceful degradation
Tab indentation Allowed Explicit error thrown Prevents hard-to-debug whitespace issues
--- delimiters Standard Standard Same behavior
Nested objects Full support Indentation-based nesting Same behavior
Array items (- item) Standard Standard Same behavior
Inline arrays ([a, b]) Standard Standard Same behavior

Tab Error Example

---
name: my-skill
metadata:
	priority: 6    # <-- Tab character: parser throws explicit error
---

The parser will throw with a message indicating the tab character and line number, making it easy to find and fix.

Frontmatter Extraction

The extractFrontmatter() function splits a SKILL.md into:

  • yaml: The raw YAML string between --- delimiters
  • body: The markdown content after the closing ---

The buildSkillMap() function reads all skills/*/SKILL.md files, extracts frontmatter, parses it with parseSimpleYaml, validates the structure, and returns a Record<string, SkillConfig> keyed by skill slug.


Environment Variables Reference

Plugin-Controlled Variables

These are set and read by the plugin's hooks. Writers and readers are listed to show data flow.

Variable Default Writer(s) Reader(s) Lifecycle
VERCEL_PLUGIN_SEEN_SKILLS "" session-start-seen-skills (init), pretooluse-skill-inject (append), user-prompt-submit (append) pretooluse-skill-inject, user-prompt-submit Session-scoped
VERCEL_PLUGIN_LIKELY_SKILLS session-start-profiler pretooluse-skill-inject, subagent-start-bootstrap Session-scoped
VERCEL_PLUGIN_GREENFIELD session-start-profiler inject-claude-md Session-scoped
VERCEL_PLUGIN_SETUP_MODE session-start-profiler pretooluse-skill-inject Session-scoped
VERCEL_PLUGIN_BOOTSTRAP_HINTS session-start-profiler Session-scoped
VERCEL_PLUGIN_RESOURCE_HINTS session-start-profiler Session-scoped
VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE session-start-profiler pretooluse-skill-inject Session-scoped
VERCEL_PLUGIN_TSX_EDIT_COUNT 0 pretooluse-skill-inject pretooluse-skill-inject Session-scoped, counter
VERCEL_PLUGIN_DEV_VERIFY_COUNT 0 pretooluse-skill-inject pretooluse-skill-inject Session-scoped, counter
VERCEL_PLUGIN_DEV_COMMAND pretooluse-skill-inject pretooluse-skill-inject Session-scoped
VERCEL_PLUGIN_VALIDATED_FILES posttooluse-validate posttooluse-validate Session-scoped
VERCEL_PLUGIN_RECENT_EDITS pretooluse-skill-inject posttooluse-verification-observe Session-scoped

User-Configurable Variables

These can be set by the user to customize plugin behavior.

Variable Default Effect
VERCEL_PLUGIN_LOG_LEVEL off Logging verbosity: off, summary, debug, trace
VERCEL_PLUGIN_DEBUG Legacy: 1 maps to debug level
VERCEL_PLUGIN_HOOK_DEBUG Legacy: 1 maps to debug level
VERCEL_PLUGIN_HOOK_DEDUP off to disable dedup entirely
VERCEL_PLUGIN_INJECTION_BUDGET 18000 PreToolUse byte budget (bytes)
VERCEL_PLUGIN_PROMPT_INJECTION_BUDGET 8000 UserPromptSubmit byte budget (bytes)
VERCEL_PLUGIN_REVIEW_THRESHOLD 3 Number of TSX edits before injecting react-best-practices
VERCEL_PLUGIN_AUDIT_LOG_FILE Path to audit log file, or off to disable
VERCEL_PLUGIN_LEXICAL_RESULT_MIN_SCORE 5.0 Minimum score for lexical fallback results

Claude Code-Provided Variables

These are set by Claude Code itself and used by hooks.

Variable Description
CLAUDE_ENV_FILE Path to env file for persisting variables across hook invocations
CLAUDE_PLUGIN_ROOT Root directory of the plugin installation
CLAUDE_PROJECT_ROOT Root directory of the user's project
SESSION_ID Fallback session ID (used when not provided in stdin)