diff --git a/eslint.config.js b/eslint.config.js
index 3b614a0df434252d290c8d01646714ac8da1d2d0..3576ce7dbb5ed244b78a4192ac0eb2cfa7ad5f9e 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,3 +1,17 @@
 import antfu from '@antfu/eslint-config'
+import enforceScriptSetupTag from './eslint_rules/enforce-script-setup-tag.js'
 
-export default antfu()
+export default antfu().append([
+    {
+        rules: {
+            'nextvs/enforce-script-setup-tag': 'error',
+        },
+        plugins: {
+            nextvs: {
+                rules: {
+                    'enforce-script-setup-tag': enforceScriptSetupTag,
+                }
+            }
+        }
+    }
+])
diff --git a/eslint_rules/enforce-script-setup-tag.js b/eslint_rules/enforce-script-setup-tag.js
new file mode 100644
index 0000000000000000000000000000000000000000..3310f452c16b3fb63a5d036918c571a66a18a9d0
--- /dev/null
+++ b/eslint_rules/enforce-script-setup-tag.js
@@ -0,0 +1,43 @@
+export default {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'Enforce <script> to have the \'setup\' attribute',
+      category: 'Best Practices',
+      recommended: false,
+    },
+    fixable: 'code',
+    schema: [],
+  },
+  create(context) {
+    return {
+      Program(_node) {
+        const sourceCode = context.getSourceCode()
+        const tokens = sourceCode.ast.tokens
+        const code = sourceCode.getText()
+        const file_name = context.getFilename()
+        if (!file_name.endsWith('.vue')) {
+          return
+        }
+        if (file_name.includes('node_modules')) {
+          return
+        }
+
+        tokens.forEach((token) => {
+          if (token.type === 'Punctuator' && token.value === '<script>') {
+            const script_tag = code.slice(token.range[0], token.range[1])
+            if (!/\bsetup\b/.test(script_tag)) {
+              context.report({
+                node: token,
+                message: '<script> tags must have the \'setup\' attribute.',
+                fix(fixer) {
+                  return fixer.replaceTextRange([token.range[0], token.range[0] + '<script'.length], '<script setup')
+                },
+              })
+            }
+          }
+        })
+      },
+    }
+  },
+}