diff --git a/rules/vue/patterns.md b/rules/vue/patterns.md
new file mode 100644
index 00000000..30698993
--- /dev/null
+++ b/rules/vue/patterns.md
@@ -0,0 +1,412 @@
+---
+paths:
+ - "**/*.vue"
+ - "**/components/**/*.ts"
+ - "**/composables/**/*.ts"
+ - "**/stores/**/*.ts"
+ - "**/pages/**/*.vue"
+---
+# Vue Patterns
+
+> This file extends [typescript/patterns.md](../typescript/patterns.md) and [common/patterns.md](../common/patterns.md) with Vue-specific architecture patterns. For composable rules see [hooks.md](./hooks.md).
+
+## Component Design Principles
+
+### Presentational vs Container
+
+Split large views into container (data-fetching, state, orchestration) and presentational (props-in, events-out) components.
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+ {{ user.name }}
+
+
+```
+
+### Provide / Inject
+
+Use for dependency injection (not state management). Ideal for: theme, locale, configuration, plugin API surfaces.
+
+```ts
+// Provider — in a parent or plugin
+const theme = ref("light");
+provide("theme", readonly(theme));
+
+// Consumer — in any descendant
+const theme = inject>("theme");
+```
+
+- Always use `readonly()` when providing to prevent child mutations.
+- Use `Symbol` keys for injection to avoid name collisions.
+- Document the injection key type with a shared constant.
+
+### Scoped Slots
+
+Use scoped slots when a child component owns data but the parent controls rendering.
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## State Management
+
+### Decision Tree
+
+1. **Component-local**: `ref()` / `reactive()` inside the component
+2. **Shared between parent + few children**: Lift to parent, pass via props + emits
+3. **Shared across distant branches, infrequent updates**: `provide` / `inject`
+4. **Global, shared, complex**: Pinia store
+5. **Server-derived data**: Composables wrapping `fetch` / `useFetch` (Nuxt) / TanStack Query (Vue Query)
+
+### Pinia Patterns
+
+```ts
+// stores/useUserStore.ts
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import { getUser, updateUser } from "@/api/user";
+
+export const useUserStore = defineStore("user", () => {
+ // State
+ const currentUser = ref(null);
+ const isLoading = ref(false);
+ const error = ref(null);
+
+ // Getters (computed)
+ const isLoggedIn = computed(() => currentUser.value !== null);
+ const displayName = computed(() =>
+ currentUser.value ? currentUser.value.name : "Guest"
+ );
+
+ // Actions
+ async function fetchUser(id: string) {
+ isLoading.value = true;
+ error.value = null;
+ try {
+ currentUser.value = await getUser(id);
+ } catch (e) {
+ error.value = e as Error;
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ return { currentUser, isLoading, error, isLoggedIn, displayName, fetchUser };
+});
+```
+
+- Prefer **Setup Store** syntax (Composition API) over Options Store.
+- Store actions are the ONLY place to mutate state — no direct `store.$patch` in components for complex logic.
+- Every async action must handle loading, success, and error states.
+- Keep stores focused on one domain — split auth, user, cart, etc. into separate stores.
+
+## Vue Router Patterns
+
+### Navigation Guards
+
+```ts
+// Global guard
+router.beforeEach((to, from) => {
+ const store = useUserStore();
+ if (to.meta.requiresAuth && !store.isLoggedIn) {
+ return { name: "login", query: { redirect: to.fullPath } };
+ }
+});
+```
+
+- Always provide a redirect path so the user returns to their intended destination after login.
+- Route guards should not have side effects beyond navigation decisions.
+- Use `beforeEnter` on routes for route-specific checks; `beforeEach` for global ones.
+
+### Lazy Loading
+
+```ts
+const routes = [
+ {
+ path: "/dashboard",
+ component: () => import("@/pages/Dashboard.vue"), // lazy
+ },
+ {
+ path: "/settings",
+ component: () => import("@/pages/Settings.vue"),
+ // Provide loading/error components
+ meta: {
+ __loadingComponent: LoadingSpinner,
+ __errorComponent: ErrorView,
+ },
+ },
+];
+```
+
+### Route Params inside Same Component
+
+```vue
+
+```
+
+## List Rendering
+
+### `v-for` with Stable Keys
+
+```vue
+
+
+ {{ item.name }}
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
{{ item.name }}
+
+```
+
+## Forms
+
+### v-model Patterns
+
+```vue
+
+
+
+
+
+
+```
+
+### Form Validation
+
+For non-trivial forms, use a vetted library:
+
+- **VeeValidate** — declarative validation rules, form-level context.
+- **FormKit** — schema-based forms with built-in validation.
+- **Custom with composable** — for simple cases only.
+
+```ts
+// Anti-pattern: manual validation in component
+const errors = ref([]);
+function submit() {
+ errors.value = [];
+ if (!email.value.includes("@")) errors.value.push("Invalid email");
+ // ... fragile, not reusable, no i18n
+}
+```
+
+### Event Handling
+
+```vue
+
+
+
+
+
+
+
+
+Link
+```
+
+## Scoped CSS
+
+```vue
+
+```
+
+- Always use `
+```
+
+## Teleport
+
+Use `` for modals, tooltips, notifications — content that must escape parent overflow/z-index constraints.
+
+```vue
+
+
+
+
+
+
+
+```
+
+**Vue 3.5+**: `` supports `defer` prop for deferred mounting. This allows teleporting to a target element that is rendered later in the same render cycle:
+
+```vue
+
+
+
Teleported content
+
+
+```
+
+## KeepAlive
+
+Cache component state when toggling between views. Always set `:max` to control memory.
+
+```vue
+
+
+
+
+
+```
+
+## `useId()` (Vue 3.5+)
+
+Generate unique, SSR-stable IDs for form elements and accessibility attributes:
+
+```vue
+
+
+
+
+
+```
+
+- IDs are unique per application instance and stable across server/client rendering.
+- Prefer `useId()` over manual ID generation to avoid SSR hydration mismatches.
+
+## `data-allow-mismatch` (Vue 3.5+)
+
+Suppress unavoidable server/client value mismatch warnings:
+
+```vue
+{{ date.toLocaleString() }}
+
+{{ clientOnlyValue }}
+```
+
+Allowed types: `text`, `children`, `class`, `style`, `attribute`.
+
+## Suspense (Experimental / Vue 3.3+)
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+## Dynamic Components
+
+```vue
+
+
+
+
+
+```
+
+## Out of Scope (Pointer Sections)
+
+### Nuxt-specific Patterns
+
+Nuxt auto-imports, server routes, Nitro, modules, and build configuration are treated as a separate framework concern. When adding deep Nuxt-specific patterns, see `skills/nuxt4-patterns/` if present, or propose a dedicated `rules/nuxt/` track.
+
+### Vue 2 / Migration
+
+Options API, `Vue.extend`, `Vue.directive`, filters, and event bus patterns belong to migration documentation. New code should target Vue 3 Composition API.
+
+## Skill Reference
+
+For Vue deep dives see `skills/vue-patterns/SKILL.md`. For cross-framework frontend concerns see `skills/frontend-patterns/SKILL.md`. For accessibility see `skills/accessibility/SKILL.md`.
diff --git a/rules/vue/security.md b/rules/vue/security.md
new file mode 100644
index 00000000..699b4426
--- /dev/null
+++ b/rules/vue/security.md
@@ -0,0 +1,250 @@
+---
+paths:
+ - "**/*.vue"
+ - "**/components/**/*.ts"
+ - "**/composables/**/*.ts"
+ - "**/pages/**/*.vue"
+ - "**/server/**/*.ts"
+---
+# Vue Security
+
+> This file extends [typescript/security.md](../typescript/security.md) and [common/security.md](../common/security.md) with Vue-specific security rules.
+
+## XSS via `v-html`
+
+CRITICAL. `v-html` sets `innerHTML` directly — Vue deliberately named it to look dangerous.
+
+```vue
+
+
+
+
+
{{ userBio }}
+
+
+
+
+
+
+```
+
+Audit checklist for every `v-html` usage:
+
+- Is the input always under our control? Document the source.
+- If user-derived: is it sanitized at the same call site?
+- Is the sanitizer allowlisting tags, not denylisting?
+- Consider `eslint-plugin-vue` rule `vue/no-v-html` to flag all usages.
+
+## Unsafe URL Bindings
+
+```vue
+
+Visit
+
+
+
+
+
+ Visit
+
+```
+
+## Template Injection via Interpolation
+
+Vue template interpolation (`{{ }}`) automatically escapes HTML entities — this is safe. The risk is `v-html` (covered above) and any custom directive that manipulates `innerHTML` directly.
+
+```ts
+// Suspicious: custom directive manipulating innerHTML
+app.directive("render-html", (el, binding) => {
+ el.innerHTML = binding.value; // Same risk as v-html
+});
+```
+
+## Secret Exposure via Environment Variables
+
+| Framework | Public prefix | Private |
+|-----------|---------------|---------|
+| Vite | `VITE_*` | Others |
+| Nuxt | `public` in `runtimeConfig` | Server-side only |
+| Vue CLI | `VUE_APP_*` | Others |
+| Custom (import.meta.env) | Any exposed via Vite `define` | Not configured |
+
+```ts
+// CRITICAL: secret leaked to client bundle (Vite)
+const apiKey = import.meta.env.VITE_STRIPE_SECRET; // ❌ VITE_ prefix = public
+
+// CORRECT: server-side only
+// vite.config.ts — never pass VITE_ prefixed secrets
+```
+
+### Nuxt Runtime Config
+
+```ts
+// nuxt.config.ts
+export default defineNuxtConfig({
+ runtimeConfig: {
+ // Server-side only — never exposed to client
+ stripeSecret: "",
+
+ // Public — exposed to client, treat as public
+ public: {
+ apiBase: "https://api.example.com",
+ // NEVER put secrets here
+ },
+ },
+});
+```
+
+### SSR Hydration Mismatch (Vue 3.5+)
+
+If server and client render different values for the same DOM node (e.g., locale-dependent date formatting), use `data-allow-mismatch` to suppress the warning rather than suppressing legitimate differences:
+
+```vue
+{{ date.toLocaleString() }}
+```
+
+Do NOT use `data-allow-mismatch` to hide real security issues like missing auth checks or mismatched auth state.
+
+## Server API Input Validation (Nuxt Nitro)
+
+```ts
+// server/api/users/[id].ts
+import { z } from "zod";
+
+const paramsSchema = z.object({
+ id: z.string().uuid(),
+});
+
+export default defineEventHandler(async (event) => {
+ // Validate route params
+ const { id } = await getValidatedRouterParams(event, paramsSchema.parse);
+
+ // Validate query
+ const query = await getValidatedQuery(event, z.object({
+ include: z.string().optional(),
+ }).parse);
+
+ // Validate body (for POST/PUT)
+ const body = await readValidatedBody(event, z.object({
+ name: z.string().min(1).max(100),
+ email: z.string().email(),
+ }).safeParse);
+
+ if (!body.success) {
+ throw createError({ statusCode: 400, message: body.error.message });
+ }
+
+ // ... proceed with validated data
+});
+```
+
+- **Never trust `event.node.req` raw properties** — use Nitro's `getValidatedRouterParams`, `readValidatedBody`, `getValidatedQuery`.
+- Server routes with write operations must validate authentication and authorization.
+- Rate-limit sensitive endpoints.
+
+## `localStorage` / `sessionStorage`
+
+```ts
+// CRITICAL: session tokens in localStorage
+localStorage.setItem("token", jwt); // ❌ any XSS can read this
+
+// CORRECT: httpOnly cookie set by server
+// Client never touches the token directly.
+```
+
+In SSR (Nuxt), `localStorage` does not exist on the server — accessing it unconditionally crashes.
+
+```ts
+// CORRECT: guard browser-only APIs
+if (import.meta.client) {
+ const theme = localStorage.getItem("theme");
+}
+```
+
+## `target="_blank"`
+
+```vue
+
+External
+
+
+External
+```
+
+Modern browsers default to `noopener`, but explicit is safer.
+
+## Third-Party Vue Libraries
+
+- Audit `npm audit` before adding any UI library.
+- Check that component libraries do not internally use `v-html` or `innerHTML` on user input.
+- Pin versions, review changelogs before major upgrades.
+- Be wary of rich-text/WYSIWYG editor components — they must sanitize HTML input.
+
+## Content Security Policy (CSP)
+
+Minimum acceptable CSP for a Vue SPA:
+
+```
+default-src 'self';
+script-src 'self' 'nonce-{REQUEST_NONCE}';
+style-src 'self' 'unsafe-inline';
+img-src 'self' data: https:;
+connect-src 'self' https://api.example.com;
+frame-ancestors 'none';
+```
+
+- For SSR (Nuxt), use per-request nonces via `useHead` / `useServerHead`.
+- Avoid `'unsafe-eval'` — Vue does not need it (unlike older Angular).
+- `style-src 'unsafe-inline'` is often required for `