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.
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
- Lifecycle Overview
- SessionStart Phase
- PreToolUse Phase
- UserPromptSubmit Phase
- PostToolUse Phase
- SubagentStart Phase
- SubagentStop Phase
- SessionEnd Phase
- Hook I/O Contract
- Custom YAML Parser Semantics
- 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_FILEfrom environment (required —requireEnvFile()exits if missing) - Appends a single
exportline — 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:
- Determine which skills are likely relevant (
VERCEL_PLUGIN_LIKELY_SKILLS) - Detect bootstrap/setup signals (
VERCEL_PLUGIN_BOOTSTRAP_HINTS,VERCEL_PLUGIN_SETUP_MODE) - Detect greenfield (empty) projects (
VERCEL_PLUGIN_GREENFIELD) - Check if
agent-browserCLI is available (VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE) - 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:seedscripts - Auth:
next-auth,@auth/core,better-authdependencies - 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.jsonindicate 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:
- Parses the tool input (file path or bash command)
- Matches against all skills'
pathPatterns,bashPatterns, andimportPatterns - Applies priority boosters (profiler, vercel.json, setup mode)
- Deduplicates against already-injected skills
- 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):
VERCEL_PLUGIN_RECENT_EDITS— file paths recently edited, e.g.app/settings/page.tsx->/settings- 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 filemessage: Error description returned to the agentseverity:error(mandatory fix) orwarn(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---delimitersbody: 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) |