- Build hooks:
bun run build:hooks(compileshooks/src/*.mts→hooks/*.mjsvia tsup) - Build from skills:
bun run build:from-skills(compiles*.md.tmpl→*.mdby resolving{{include:skill:…}}markers) - Check from skills:
bun run build:from-skills:check(verify generated.mdfiles are up-to-date; exits non-zero on drift) - Build all:
bun run build:all(skills + hooks + from-skills). It is not namedbuild: npm then "prepares" Git installs, which breaksopencode plugin add github:vercel/vercel-plugin(anomalyco/opencode#49704) - Test:
bun test(typecheck + 42 test files) - Single test:
bun test tests/<file>.test.ts - Typecheck only:
bun run typecheck(tsc on hooks/tsconfig.json) - Validate skills:
bun run validate(structural validation of skill sources; does not write generated files) - Doctor:
bun run doctor(self-diagnosis: skill validation, hook timeouts, dedup health) - Update snapshots:
bun test:update-snapshots(regenerate golden snapshot baselines) - Playground:
bun run playground:generate(generate static skill files for external tools)
Run bun run build:hooks after editing any .mts file. A pre-commit hook auto-compiles when .mts files are staged.
Run bun run build:from-skills after editing any skill referenced by a .md.tmpl template. The build:all script includes this step automatically.
All hooks are registered in hooks/hooks.json and run via node "${CLAUDE_PLUGIN_ROOT}/hooks/<file>.mjs". Hook output is type-checked against SyncHookJSONOutput from @anthropic-ai/claude-agent-sdk.
| Event | Hook | Matcher | Timeout |
|---|---|---|---|
| SessionStart | session-start-seen-skills.mjs |
startup|resume|clear|compact |
— |
| SessionStart | session-start-profiler.mjs |
startup|resume|clear|compact |
— |
| SessionStart | inject-claude-md.mjs |
startup|resume|clear|compact |
— |
| PostToolUse | posttooluse-skill-telemetry.mjs |
Skill |
5s |
| SessionEnd | session-end-cleanup.mjs |
— | — |
Source lives in hooks/src/*.mts (TypeScript) and compiles to hooks/*.mjs (ESM, committed).
Entry-point hooks (wired in hooks.json):
session-start-seen-skills.mts— initializesVERCEL_PLUGIN_SEEN_SKILLS=""inCLAUDE_ENV_FILEsession-start-profiler.mts— activates only for greenfield directories or detected Vercel, Next.js, or eve projects, then scans config files + package deps → setsVERCEL_PLUGIN_LIKELY_SKILLS(+5 priority boost)inject-claude-md.mts— outputs the thin session-start Vercel context plus knowledge update guidance for that same activation setposttooluse-skill-telemetry.mts— onSkilltool use, reports the bare name of a plugin-namespaced (vercel:/vercel-plugin:), plugin-shipped skill/command (skill:invoked); other namespaces, bare names, and arguments are dropped. Also the--sendentrypoint that the detached sender runs for every skill eventsession-end-cleanup.mts— deletes session-scoped temp files
Library modules (imported by entry-point hooks):
hook-env.mts— shared runtime helpers (env parsing, path resolution)skill-map-frontmatter.mts— YAML parser + frontmatter extraction +buildSkillMap()+validateSkillMap()patterns.mts— glob→regex conversion, seen-skills helpers, ranking, atomic file claimsprompt-patterns.mts— prompt signal compiler + scorer (phrases/allOf/anyOf/noneOf)prompt-analysis.mts— dry-run analysis reports for prompt matchingvercel-config.mts— vercel.json key→skill routing (±10 priority)logger.mts— structured JSON logging to stderr (off/summary/debug/trace)telemetry.mts— opt-out telemetry (VERCEL_PLUGIN_TELEMETRY=off): daily DAU ping, installation ID, skill slug/namespace validation, andskill:invoked/skill:injectedevent builders/sendersskill-telemetry.mts— shared skill-telemetry plumbing: per-session telemetry UUID + harness temp files, payload assembly, andqueueSkillTelemetry()which spawns the detached sender (used by the Skill hook and both inject hooks)
OpenCode loads hooks/opencode-plugin.mjs through package.json exports (opencode plugin add github:vercel/vercel-plugin). It runs inside the long-lived OpenCode server, not as per-event subprocesses, and reuses the hook modules:
- Skills, commands (
vercel:<name>), agents (subagents), and the.mcp.jsonservers are registered through OpenCode transforms. User definitions with the same ID win. - The SessionStart profiler and
inject-claude-mdoutput becomes acontexthook, computed once per lead session and reused verbatim. Child sessions get nothing, like Claude Code subagents. - Skill telemetry is a
toolexecute.afterhook on OpenCode'sskilltool, reported with theopencodeharness. State is in memory, so there is no SessionEnd cleanup.
To try a local checkout, create ~/.config/opencode/plugins/vercel-plugin.js containing export { default } from "<checkout>/hooks/opencode-plugin.mjs";. A configured local directory path does not work: OpenCode only reads index.* from local plugin directories (anomalyco/opencode#52300).
- SessionStart: For greenfield directories or detected Vercel, Next.js, or eve projects, the profiler scans the project → sets
VERCEL_PLUGIN_LIKELY_SKILLS - PreToolUse (on Read/Edit/Write/Bash): Match file paths (glob), bash commands (regex), imports (regex+flags) → apply vercel.json routing → apply profiler boost → rank by priority → dedup → inject up to 3 skills within 18KB budget
- UserPromptSubmit: Score prompt text against
promptSignals(phrases/allOf/anyOf/noneOf) → inject up to 2 skills within 8KB budget- 3b. Lexical fallback (when
VERCEL_PLUGIN_LEXICAL_PROMPT=on): If phrase/allOf/anyOf scoring yields no matches aboveminScore, re-score using a lexical stemmer that normalizes prompt tokens before comparison — catches natural phrasing that exact-substring matching misses
- 3b. Lexical fallback (when
- PostToolUse (Skill): If the loaded skill is one shipped by this plugin under its namespace, emit a
skill:invokedtelemetry event (name only, tagged with the session's harness) from a detached background process. The inject hooks in steps 2–3 emitskill:injectedthe same way when they are wired in - SessionEnd: Clean up session-scoped temp files
Special triggers in PreToolUse:
- TSX review: After N
.tsxedits (default 3), injectsreact-best-practices - Dev server detection: Boosts
agent-browser-verifywhen dev server patterns appear - Vercel env help: One-time injection for
vercel envcommands
38 skills in skills/. Each has a SKILL.md with YAML frontmatter:
---
name: skill-slug
description: "One-line description"
summary: "Brief fallback (injected when budget exceeded)"
metadata:
priority: 6 # 4-8 range; higher = injected first
pathPatterns: ["glob1"] # File glob patterns
bashPatterns: ["regex1"] # Bash command regex patterns
importPatterns: ["package"] # Import/require patterns
promptSignals: # UserPromptSubmit scoring
phrases: ["key phrase"] # +6 each (exact substring, case-insensitive)
allOf: [["term1", "term2"]] # +4 per group (all must match)
anyOf: ["optional"] # +1 each, capped at +2
noneOf: ["exclude"] # Hard suppress (score → -Infinity)
minScore: 6 # Threshold (default 6)
validate: # PostToolUse validation rules
- pattern: "regex"
message: "Error description"
severity: "error|recommended|warn"
skipIfFileContains: "regex" # Optional conditional skip
---
# Skill body (markdown, injected as additionalContext)Hooks and developer tools read metadata directly from skills/*/SKILL.md using buildSkillMap() and validateSkillMap(). Matching patterns are compiled in memory for each invocation, so skill edits are picked up without rebuilding a separate index. CLI scripts share src/shared/skill-map-loader.ts.
Prevents the same skill from being injected twice in a session. Shared across PreToolUse and UserPromptSubmit hooks.
- Claim dir:
<tmpdir>/vercel-plugin-<sessionId>-seen-skills.d/— one empty file per claimed skill, created atomically withopenSync(path, "wx")(O_EXCL) - Session file:
<tmpdir>/vercel-plugin-<sessionId>-seen-skills.txt— comma-delimited snapshot synced from claim dir - Env var:
VERCEL_PLUGIN_SEEN_SKILLS— initialized by session-start, updated by hooks - State merge:
mergeSeenSkillStates()unions all three sources - Cleanup:
session-end-cleanup.mjsdeletes temp files + claim dirs - Strategies (debug mode):
"file"(atomic claims) →"env-var"(fallback) →"memory-only"(single invocation) →"disabled"(VERCEL_PLUGIN_HOOK_DEDUP=off)
Uses inline parseSimpleYaml in skill-map-frontmatter.mjs, not js-yaml:
- Bare
null→ string"null", not JavaScriptnull - Bare
true/false→ strings, not booleans - Unclosed
[→ scalar string, not parse error - Tab indentation → explicit error
vercel-plugin explain <target> [--json] [--project <path>] [--likely-skills s1,s2] [--budget <bytes>]— shows which skills match a file path or bash command, with priority breakdown and budget simulationvercel-plugin doctor— validates skill metadata, hook timeout risk, dedup correctness, and template freshness
Generates static skill files for external tools (Cursor, VSCode Copilot, Gemini CLI, etc.). Run bun run playground:generate. Generators live in .playground/<tool-name>/, fixtures in .playground/_fixtures/, snapshots in .playground/_snapshots/.
Agents and commands derive instructions from skills via .md.tmpl templates. Skills are the single source of truth — templates pull content at build time so agents/commands stay in sync without duplicating prose.
Convention: agents/<name>.md.tmpl and commands/<name>.md.tmpl compile to agents/<name>.md and commands/<name>.md (committed). Two include marker formats:
{{include:skill:<name>:<heading>}} — extracts a markdown section by heading
{{include:skill:<name>:frontmatter:<field>}} — extracts a frontmatter field value
Heading extraction is case-insensitive and captures everything from the heading to the next heading of equal or higher level.
Build: bun run build:from-skills resolves all includes and writes output files. It is part of bun run build. bun run build:from-skills:check verifies outputs are up-to-date in CI. Dependency queries (--skill <name>) and coverage reports (--audit) are calculated from templates on demand.
Current templates (7): agents/ai-architect.md.tmpl, agents/deployment-expert.md.tmpl, agents/performance-optimizer.md.tmpl, commands/bootstrap.md.tmpl, commands/deploy.md.tmpl, commands/env.md.tmpl, commands/status.md.tmpl.
42 test files across tests/. Key categories:
- Hook integration:
session-start-profiler,session-start-seen-skills - Pattern matching:
patterns,fuzz-glob,fuzz-yaml,prompt-signals,prompt-analysis - Snapshots:
snapshot-runner(golden snapshots of skill injection metadata per vercel.json fixture),snapshots(snapshot assertions) - Validation:
validate,validate-rules,build-skill-map - Benchmark:
benchmark-pipeline,benchmark-analyze - CLI:
cli-explain - Specialized:
notion-clone-patterns,slack-clone-patterns,tsx-review-trigger,dev-server-verify,subagent-fresh-env,session-timeline-subagent
Snapshot updates: bun run test:update-snapshots (sets UPDATE_SNAPSHOTS=1).
| Variable | Default | Description |
|---|---|---|
VERCEL_PLUGIN_LOG_LEVEL |
off |
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_SEEN_SKILLS |
"" |
Comma-delimited already-injected skills |
VERCEL_PLUGIN_HOOK_DEDUP |
— | off to disable dedup entirely |
VERCEL_PLUGIN_LIKELY_SKILLS |
— | Profiler-set comma-delimited skills (+5 boost) |
VERCEL_PLUGIN_GREENFIELD |
— | true if project is empty (profiler sets) |
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 injection |
VERCEL_PLUGIN_TSX_EDIT_COUNT |
0 |
Current .tsx edit count (PreToolUse tracks) |
VERCEL_PLUGIN_AUDIT_LOG_FILE |
— | Audit log path or off |
VERCEL_PLUGIN_LEXICAL_PROMPT |
on |
0 to disable lexical stemmer fallback in UserPromptSubmit scoring |
VERCEL_PLUGIN_TELEMETRY |
— | off disables all telemetry (DAU ping, skill:invoked, skill:injected). bunfig.toml preloads tests/_preload-telemetry-off.ts so the test suite never phones home |