Skip to content

Commit 211691e

Browse files
authored
feat(nuxt-ui): add PreToolUse hook for component guidance (#121)
* docs(track): add nuxt-ui-hook-guidance-20260328 Add track for PreToolUse hook in nuxt-ui plugin that guides LLMs to use Nuxt UI MCP tools when writing Vue files with U* components. * chore(track): nuxt-ui-hook-guidance-20260328 start implementation * feat(nuxt-ui): add PreToolUse hook for component guidance Shell script that detects Nuxt UI components (U* prefix) in .vue files during Write/Edit operations and injects MCP tool guidance to prevent common v3→v4 migration mistakes from stale LLM training data. * feat(nuxt-ui): register PreToolUse hook in hooks.json Configure Write|Edit matcher to trigger component detection hook with 5s timeout using CLAUDE_PLUGIN_ROOT for portable paths. * docs(track): mark all tasks complete for nuxt-ui-hook-guidance-20260328 * fix(nuxt-ui): use correct MCP tool prefix nuxt-ui-remote HTTP-type MCP servers in Claude Code get a -remote suffix appended to their key name. Updated tool references from mcp__nuxt-ui__ to mcp__nuxt-ui-remote__ to match actual runtime tool names. * docs(track): add retrospective for nuxt-ui-hook-guidance-20260328 * chore: apply AI code review suggestions for check-vue-components.sh - Use single jq call with mapfile for efficiency (avoid multiple cat/echo/jq) - Fix trailing comma+space bug in component list string construction - Use while read loop directly on newline-separated list (more robust) - Wrap component names in <component> delimiters for prompt injection mitigation * chore: apply AI code review suggestions - fix: add || true to grep pipeline in check-vue-components.sh to prevent premature exit under set -euo pipefail when no Nuxt UI components are detected - fix: correct null handling in jq mapfile to always emit 4 lines (use explicit // "" per field, not .[] // "") - fix: assert_empty in test now captures exit code and treats non-zero as test failure instead of || true masking - fix: update spec.md FR-4 and plan.md observable outcomes to use consistent mcp__nuxt-ui-remote__ namespace
1 parent 2834948 commit 211691e

7 files changed

Lines changed: 386 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"track_id": "nuxt-ui-hook-guidance-20260328",
3+
"type": "feature",
4+
"status": "in_progress",
5+
"created_at": "2026-03-28T22:40:00+09:00",
6+
"updated_at": "2026-03-28T22:41:00+09:00",
7+
"issue": "",
8+
"pr": "",
9+
"project": ""
10+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Plan: Nuxt UI Hook Guidance
2+
3+
> Track: nuxt-ui-hook-guidance-20260328
4+
> Spec: [spec.md](./spec.md)
5+
6+
## Overview
7+
8+
- **Source**: [spec.md](./spec.md)
9+
- **Issue**: TBD
10+
- **Created**: 2026-03-28
11+
- **Approach**: Minimal Change
12+
13+
## Purpose
14+
15+
After this change, LLMs using Claude Code with the nuxt-ui plugin will receive automatic guidance to verify Nuxt UI component APIs via MCP tools before writing `.vue` files. They can verify it works by writing a `.vue` file containing `<UButton>` and observing the MCP guidance message injected by the hook.
16+
17+
## Context
18+
19+
LLMs frequently misuse Nuxt UI v4 components due to stale training data — using v3 API patterns, incorrect prop names, or missing required wrappers like `UApp`. The nuxt-ui plugin already provides a skill with comprehensive documentation and an MCP server at `https://ui.nuxt.com/mcp`, but the skill is passive — it only loads when relevant. There is no active intervention at the point of code generation.
20+
21+
The solution is a PreToolUse hook on Write/Edit tools that detects Nuxt UI components (`U*` prefix) in `.vue` file content and injects a guidance message reminding the LLM to use MCP tools (`get_component`, `list_components`) for correct API reference before proceeding.
22+
23+
The hook follows the same pattern as `plugins/fetch/hooks/webfetch-fallback.sh` — a lightweight shell script using `jq` to parse stdin JSON and emit `hookSpecificOutput` with `additionalContext`. No build step required.
24+
25+
### Non-Goals
26+
27+
- Auto-fixing incorrect component usage
28+
- Blocking or rejecting Write/Edit operations
29+
- Validating component props at the hook level
30+
- Supporting non-Vue files (`.tsx`, `.jsx`)
31+
32+
## Architecture Decision
33+
34+
Shell script approach chosen over TypeScript for several reasons: the logic is simple (regex match + message generation), it avoids a build step, follows the established fetch plugin pattern, and has zero dependencies beyond `jq` (universally available). The hook reads `tool_input` from stdin JSON, checks `file_path` for `.vue` extension, extracts content (Write: `content` field; Edit: `new_string` field), matches `<U[A-Z][a-zA-Z]+` pattern to detect Nuxt UI components, and returns a guidance message listing detected components with MCP tool call suggestions.
35+
36+
## Tasks
37+
38+
- [x] T001 Create PreToolUse hook script for Nuxt UI component detection (file: plugins/nuxt-ui/hooks/check-vue-components.sh)
39+
- [x] T002 Create hooks.json configuration for nuxt-ui plugin (file: plugins/nuxt-ui/hooks/hooks.json) (depends on T001)
40+
- [x] T003 Add hook integration test (file: plugins/nuxt-ui/hooks/check-vue-components.test.sh) (depends on T001)
41+
42+
## Key Files
43+
44+
### Create
45+
46+
- `plugins/nuxt-ui/hooks/hooks.json` — Hook configuration registering PreToolUse matcher for Write and Edit
47+
- `plugins/nuxt-ui/hooks/check-vue-components.sh` — Shell script that parses tool input, detects U* components in .vue files, returns MCP guidance message
48+
49+
### Reuse
50+
51+
- `plugins/nuxt-ui/.claude-plugin/plugin.json` — Existing plugin manifest (no changes needed; hooks.json auto-loaded)
52+
- `plugins/fetch/hooks/webfetch-fallback.sh` — Reference implementation for hookSpecificOutput pattern
53+
54+
## Verification
55+
56+
### Automated Tests
57+
58+
- [ ] Pipe Write tool input with `.vue` file containing `<UButton>` → hook returns guidance message
59+
- [ ] Pipe Write tool input with `.vue` file containing no U* components → hook returns empty (passthrough)
60+
- [ ] Pipe Edit tool input with `new_string` containing `<UModal>` in `.vue` file → hook returns component-specific guidance
61+
- [ ] Pipe Write tool input with `.ts` file → hook returns empty (non-vue passthrough)
62+
- [ ] Guidance message includes detected component names and MCP tool suggestions
63+
64+
### Observable Outcomes
65+
66+
- After installing the nuxt-ui plugin and writing a `.vue` file with `<UButton>`, the LLM receives a message suggesting to call `mcp__nuxt-ui-remote__get_component` with `component: "Button"` before proceeding
67+
- Running `echo '{"tool_name":"Write","tool_input":{"file_path":"app.vue","content":"<template><UButton /></template>"}}' | bash plugins/nuxt-ui/hooks/check-vue-components.sh` shows JSON output with guidance message
68+
69+
### Manual Testing
70+
71+
- [ ] Install nuxt-ui plugin, write a .vue file with UButton — verify guidance appears
72+
- [ ] Write a .vue file without U* components — verify no guidance appears
73+
74+
### Acceptance Criteria Check
75+
76+
- [ ] AC-1: Writing `.vue` with `<UButton>` triggers MCP guidance
77+
- [ ] AC-2: Writing `.vue` without U* components produces no output
78+
- [ ] AC-3: Editing `.vue` with `UModal` in new_string triggers guidance
79+
- [ ] AC-4: Message includes MCP tool call suggestions with component names
80+
- [ ] AC-5: Hook registered in `plugins/nuxt-ui/hooks/hooks.json`
81+
82+
## Decision Log
83+
84+
- Decision: Use shell script (bash + jq) instead of TypeScript
85+
Rationale: Simple regex logic, no build step needed, follows fetch plugin pattern, zero additional dependencies
86+
Date/Author: 2026-03-28 / Claude
87+
88+
- Decision: Match on Write and Edit tools with single matcher pattern
89+
Rationale: Both tools modify .vue file content; Edit uses new_string which may contain new U* components
90+
Date/Author: 2026-03-28 / Claude
91+
92+
- Decision: Use `additionalContext` (non-blocking) instead of `decision: deny`
93+
Rationale: Hook should guide, not block — the LLM may already have correct knowledge from the skill
94+
Date/Author: 2026-03-28 / Claude
95+
96+
## Outcomes & Retrospective
97+
98+
### What Was Shipped
99+
- PreToolUse hook for nuxt-ui plugin that detects U* components in .vue files and injects MCP guidance
100+
- hooks.json configuration with Write|Edit matcher
101+
- 8-case test suite for the hook script
102+
103+
### What Went Well
104+
- Shell script approach kept it simple — no build step, single file, fast execution
105+
- TDD caught the component detection pattern early
106+
- Review caught critical MCP tool prefix mismatch (nuxt-ui vs nuxt-ui-remote for HTTP-type servers)
107+
108+
### What Could Improve
109+
- HTTP-type MCP server naming convention (`-remote` suffix) should be documented as a gotcha
110+
111+
### Tech Debt Created
112+
- None
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Nuxt UI Hook Guidance
2+
3+
> Track: nuxt-ui-hook-guidance-20260328
4+
5+
## Overview
6+
7+
Add a PreToolUse hook to the nuxt-ui plugin that intercepts Write/Edit operations on `.vue` files containing Nuxt UI components (`U*` prefix). The hook injects guidance messages reminding the LLM to use Nuxt UI MCP tools (`list_components`, `get_component`) for correct component API usage, warns about common mistakes (e.g., deprecated v3 patterns, incorrect prop names), and provides targeted advice based on detected components.
8+
9+
### Problem
10+
11+
LLMs frequently misuse Nuxt UI v4 components due to stale training data (e.g., using Nuxt UI v3 API patterns, incorrect prop names, missing required wrappers like `UApp`). The existing skill-based approach provides documentation but doesn't actively intervene at the point of code generation.
12+
13+
### Solution
14+
15+
A PreToolUse hook on Write/Edit that:
16+
1. Detects `.vue` file targets from tool arguments
17+
2. Parses the content for Nuxt UI components (`U*` prefix)
18+
3. Returns a guidance message with MCP tool reminders and component-specific warnings
19+
20+
## Requirements
21+
22+
### Functional Requirements
23+
24+
- [ ] FR-1: Hook triggers on PreToolUse for Write and Edit tools when the target file has a `.vue` extension
25+
- [ ] FR-2: Hook parses the tool arguments (file content or edit strings) to detect Nuxt UI components by `U` prefix pattern (e.g., `UButton`, `UInput`, `UModal`)
26+
- [ ] FR-3: If no Nuxt UI components are detected, hook returns no guidance (silent pass-through)
27+
- [ ] FR-4: When Nuxt UI components are detected, hook returns a message reminding the LLM to use `mcp__nuxt-ui-remote__get_component` and `mcp__nuxt-ui-remote__list_components` MCP tools to verify correct API usage before proceeding
28+
- [ ] FR-5: Hook message includes warnings about common Nuxt UI v3→v4 migration mistakes (e.g., deprecated props, renamed components, changed slot names)
29+
- [ ] FR-6: Hook message is component-specific — lists the detected components and suggests checking each one via MCP
30+
31+
### Non-functional Requirements
32+
33+
- [ ] NFR-1: Hook execution completes within 5 seconds (timeout limit)
34+
- [ ] NFR-2: Hook has zero impact when no Nuxt UI components are detected (fast path)
35+
- [ ] NFR-3: Hook script is lightweight — no external dependencies beyond Node.js built-ins
36+
37+
## Acceptance Criteria
38+
39+
- [ ] AC-1: Writing a `.vue` file containing `<UButton>` triggers hook and returns MCP guidance message
40+
- [ ] AC-2: Writing a `.vue` file with no `U*` components produces no hook output
41+
- [ ] AC-3: Editing a `.vue` file where the edit string contains `UModal` triggers component-specific guidance
42+
- [ ] AC-4: Hook message includes actionable MCP tool call suggestions (tool name + component name)
43+
- [ ] AC-5: Hook is registered in `plugins/nuxt-ui/hooks/hooks.json` with correct matcher pattern
44+
45+
## Out of Scope
46+
47+
- Auto-fixing incorrect component usage (hook only provides guidance)
48+
- Blocking or rejecting Write/Edit operations
49+
- Validating component props at the hook level (MCP tools handle this)
50+
- Supporting non-Vue files (e.g., `.tsx`, `.jsx`)
51+
52+
## Assumptions
53+
54+
- The Nuxt UI MCP server at `https://ui.nuxt.com/mcp` remains available and returns up-to-date component documentation
55+
- Nuxt UI components consistently use the `U` prefix naming convention
56+
- The hook reads tool arguments from `$TOOL_INPUT` environment variable (standard Claude Code hook protocol)

.please/docs/tracks/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
| Track | Feature | Type | Issue | Started | Status |
88
|-------|---------|------|-------|---------|--------|
9+
| [nuxt-ui-hook-guidance-20260328](active/nuxt-ui-hook-guidance-20260328/plan.md) | Nuxt UI Hook Guidance | feature | TBD | 2026-03-28 | in_progress |
910

1011
## Recently Completed
1112

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/bin/bash
2+
# PreToolUse hook: Detect Nuxt UI components in .vue files and provide MCP guidance.
3+
# Reads JSON from stdin (tool_name, tool_input). Outputs hookSpecificOutput JSON on stdout.
4+
set -euo pipefail
5+
6+
mapfile -t values < <(jq -r '[.tool_name, .tool_input.file_path, (.tool_input.content // ""), (.tool_input.new_string // "")] | .[]')
7+
8+
tool_name=${values[0]:-""}
9+
file_path=${values[1]:-""}
10+
11+
# Only handle Write and Edit tools
12+
case "$tool_name" in
13+
Write|Edit) ;;
14+
*) exit 0 ;;
15+
esac
16+
17+
# Only handle .vue files
18+
case "$file_path" in
19+
*.vue) ;;
20+
*) exit 0 ;;
21+
esac
22+
23+
# Extract relevant content based on tool type
24+
if [ "$tool_name" = "Write" ]; then
25+
content=${values[2]:-""}
26+
elif [ "$tool_name" = "Edit" ]; then
27+
# Only check new_string — we care about components being added, not removed
28+
content=${values[3]:-""}
29+
fi
30+
31+
if [ -z "$content" ]; then
32+
exit 0
33+
fi
34+
35+
# Detect Nuxt UI components (U followed by uppercase letter)
36+
components_list=$(echo "$content" | grep -oE '<U[A-Z][a-zA-Z]+' | sed 's/^<//' | sort -u || true)
37+
38+
if [ -z "$components_list" ]; then
39+
exit 0
40+
fi
41+
42+
components="<components>$(echo "$components_list" | tr '\n' ', ' | sed 's/, $//')</components>"
43+
44+
# Build component-specific MCP suggestions
45+
mcp_suggestions=""
46+
while IFS= read -r comp; do
47+
if [ -z "$comp" ]; then continue; fi
48+
# Strip the leading U to get the component name for MCP
49+
comp_name=$(echo "$comp" | sed 's/^U//')
50+
mcp_suggestions="${mcp_suggestions} - mcp__nuxt-ui-remote__get_component(component: \"${comp_name}\") for <component>${comp}</component> API reference\n"
51+
done <<< "$components_list"
52+
53+
# Build guidance message
54+
message="Nuxt UI components detected: ${components}
55+
56+
IMPORTANT: LLM training data may contain outdated Nuxt UI v3 patterns. Before proceeding, verify correct v4 API usage:
57+
58+
${mcp_suggestions}
59+
Common v3→v4 mistakes to avoid:
60+
- UFormGroup is now UFormField
61+
- USelectMenu is now USelect
62+
- Modal/Slideover use v-model:open instead of v-model
63+
- DropdownMenu items use flat array with { type: 'separator' } instead of nested arrays for groups
64+
- UCard slots: header/body/footer (not default slot for body content)
65+
66+
Use mcp__nuxt-ui-remote__list_components to browse all available components."
67+
68+
jq -n --arg msg "$message" '{
69+
"hookSpecificOutput": {
70+
"hookEventName": "PreToolUse",
71+
"additionalContext": $msg
72+
}
73+
}'
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/bash
2+
# Tests for check-vue-components.sh hook
3+
# Run: bash plugins/nuxt-ui/hooks/check-vue-components.test.sh
4+
5+
set -euo pipefail
6+
7+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8+
HOOK="$SCRIPT_DIR/check-vue-components.sh"
9+
PASS=0
10+
FAIL=0
11+
12+
assert_output() {
13+
local desc="$1"
14+
local input="$2"
15+
local expected_pattern="$3"
16+
17+
local output
18+
output=$(echo "$input" | bash "$HOOK" 2>/dev/null) || true
19+
20+
if echo "$output" | grep -qE "$expected_pattern"; then
21+
echo " PASS: $desc"
22+
PASS=$((PASS + 1))
23+
else
24+
echo " FAIL: $desc"
25+
echo " Input: $input"
26+
echo " Expected pattern: $expected_pattern"
27+
echo " Got: $output"
28+
FAIL=$((FAIL + 1))
29+
fi
30+
}
31+
32+
assert_empty() {
33+
local desc="$1"
34+
local input="$2"
35+
36+
local output
37+
local exit_code=0
38+
output=$(echo "$input" | bash "$HOOK" 2>/dev/null) || exit_code=$?
39+
40+
if [ "$exit_code" -ne 0 ]; then
41+
echo " FAIL: $desc (hook exited with code $exit_code)"
42+
echo " Input: $input"
43+
FAIL=$((FAIL + 1))
44+
return
45+
fi
46+
47+
if [ -z "$output" ]; then
48+
echo " PASS: $desc"
49+
PASS=$((PASS + 1))
50+
else
51+
echo " FAIL: $desc (expected empty output)"
52+
echo " Input: $input"
53+
echo " Got: $output"
54+
FAIL=$((FAIL + 1))
55+
fi
56+
}
57+
58+
echo "=== check-vue-components.sh Tests ==="
59+
echo ""
60+
61+
echo "--- Write tool with .vue file containing UButton ---"
62+
assert_output \
63+
"T1: Write .vue with UButton triggers guidance" \
64+
'{"tool_name":"Write","tool_input":{"file_path":"app.vue","content":"<template><UButton label=\"Click\" /></template>"}}' \
65+
"get_component"
66+
67+
echo ""
68+
echo "--- Write tool with .vue file without U* components ---"
69+
assert_empty \
70+
"T2: Write .vue without U* components is silent" \
71+
'{"tool_name":"Write","tool_input":{"file_path":"app.vue","content":"<template><div>Hello</div></template>"}}'
72+
73+
echo ""
74+
echo "--- Edit tool with .vue file containing UModal ---"
75+
assert_output \
76+
"T3: Edit .vue with UModal triggers guidance" \
77+
'{"tool_name":"Edit","tool_input":{"file_path":"components/Dialog.vue","old_string":"<div>","new_string":"<UModal v-model:open=\"isOpen\" title=\"Edit\">"}}' \
78+
"UModal"
79+
80+
echo ""
81+
echo "--- Write tool with .ts file (non-vue) ---"
82+
assert_empty \
83+
"T4: Write .ts file is silent" \
84+
'{"tool_name":"Write","tool_input":{"file_path":"utils.ts","content":"export const foo = 1"}}'
85+
86+
echo ""
87+
echo "--- Write tool with multiple U* components ---"
88+
assert_output \
89+
"T5: Multiple U* components listed in guidance" \
90+
'{"tool_name":"Write","tool_input":{"file_path":"page.vue","content":"<template><UCard><UButton /><UInput /></UCard></template>"}}' \
91+
"UCard.*UButton|UButton.*UCard"
92+
93+
echo ""
94+
echo "--- Guidance includes MCP tool suggestion ---"
95+
assert_output \
96+
"T6: Guidance includes mcp tool name" \
97+
'{"tool_name":"Write","tool_input":{"file_path":"page.vue","content":"<template><UButton /></template>"}}' \
98+
"mcp__nuxt-ui"
99+
100+
echo ""
101+
echo "--- Non Write/Edit tool is silent ---"
102+
assert_empty \
103+
"T7: Read tool is silent" \
104+
'{"tool_name":"Read","tool_input":{"file_path":"page.vue"}}'
105+
106+
echo ""
107+
echo "--- Edit with U* only in old_string (removal) should still be silent ---"
108+
assert_empty \
109+
"T8: Edit removing U* component (only in old_string) is silent" \
110+
'{"tool_name":"Edit","tool_input":{"file_path":"page.vue","old_string":"<UButton />","new_string":"<button>Click</button>"}}'
111+
112+
echo ""
113+
echo "=== Results: $PASS passed, $FAIL failed ==="
114+
115+
if [ "$FAIL" -gt 0 ]; then
116+
exit 1
117+
fi

plugins/nuxt-ui/hooks/hooks.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"description": "Nuxt UI component guidance: reminds LLMs to verify v4 API via MCP before writing Vue files",
3+
"hooks": {
4+
"PreToolUse": [
5+
{
6+
"matcher": "Write|Edit",
7+
"hooks": [
8+
{
9+
"type": "command",
10+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/check-vue-components.sh",
11+
"timeout": 5
12+
}
13+
]
14+
}
15+
]
16+
}
17+
}

0 commit comments

Comments
 (0)