@clerk/eslint-plugin
@clerk/eslint-plugin provides lint rules for Clerk patterns across JavaScript frameworks. It currently includes one Next.js App Router rule, @clerk/next/require-auth-protection, which flags server-side resources that are missing an authentication check with either await auth.protect() or an equivalent auth()
Install @clerk/eslint-plugin as a development dependency:
npm install --save-dev @clerk/eslint-pluginpnpm add --save-dev @clerk/eslint-pluginyarn add --dev @clerk/eslint-pluginbun add --dev @clerk/eslint-pluginThe plugin requires ESLint >=9 and the flat config format. It can also run with Oxlint when configured as a JavaScript plugin. ESLint is required even in Oxlint setups, because the Bulk Fixer CLI runs against an ESLint instance.
Configure the plugin in your lint config. The following examples protect all resources by default and opt out of public sign-in and sign-up routes; adapt the protected and public paths to your application structure. See the Rule options
import clerkNext from '@clerk/eslint-plugin/next'
export default [
{
plugins: { '@clerk/next': clerkNext },
rules: {
'@clerk/next/require-auth-protection': [
'error',
{
protected: ['**'],
public: ['src/app/sign-in/**', 'src/app/sign-up/**'],
},
],
},
},
]{
"jsPlugins": [
{
"name": "@clerk/next",
"specifier": "@clerk/eslint-plugin/next"
}
],
"rules": {
"@clerk/next/require-auth-protection": [
"error",
{
"protected": ["**"],
"public": ["src/app/sign-in/**", "src/app/sign-up/**"]
}
]
}
}Path matching
protected and public patterns are folder globs relative to rootDir. Use app/** for a root app directory, src/app/** for a src/app directory, and src/** or shared/** for other project folders.
Globs support a small syntax:
*matches one path segment.**matches any number of path segments.
Patterns must use / separators and must be relative. The dialect doesn't support empty path segments, . segments, .. segments, or brace expansion; using any of them throws a configuration error and fails the lint run.
When a folder matches both protected and public, the most specific pattern wins. If both patterns have the same specificity, protected wins.
It's recommended to protect all resources by default, then exempting public routes:
const clerkProtection = {
protected: ['**'],
public: ['src/app/sign-in/**', 'src/app/sign-up/**', 'src/actions/public/**'],
}You can make the default public and opt into protected folders, but Clerk doesn't recommend using public: ['**']. A Server Function file imported from a folder matched by public is skipped, even when it's called from a protected page.
If you need a public default for routes, prefer a narrower public pattern:
const clerkProtection = {
public: ['src/app/**'],
protected: [
// Protect everything outside src/app/.
'**',
// Or opt into protection for a specific route group in `src/app/`.
'src/app/(dashboard)/**',
],
}Monorepos
If each application has its own eslint.config.* file, the rule can usually resolve paths without extra configuration.
If your monorepo has a single top-level ESLint config, define the rule once for each application and set rootDir to that application's root:
import clerkNext from '@clerk/eslint-plugin/next'
export default [
{
files: ['apps/web/src/**/*.{ts,tsx}'],
plugins: { '@clerk/next': clerkNext },
rules: {
'@clerk/next/require-auth-protection': [
'error',
{
rootDir: 'apps/web',
protected: ['**'],
public: ['src/app/sign-in/**', 'src/app/sign-up/**'],
},
],
},
},
]Rule options
The @clerk/next/require-auth-protection rule accepts one options object.
- Name
protected- Type
string[]- Description
Required. Project-relative folder globs whose resources must be guarded.
- Name
public?- Type
string[]- Description
Project-relative folder globs that are exempt. Defaults to
[].
- Name
resources?- Type
{ routeHandlers?: boolean, serverFunctions?: boolean, serverComponentEntrypoints?: boolean }- Description
Resource groups to check. All resource groups are enabled by default.
routeHandlers: Checks HTTP method exports inroutefiles.serverFunctions: Checks exported Server Functions in'use server'files and inline Server Functions.serverComponentEntrypoints: Checks default exports frompage,layout,template, anddefaultfiles.
- Name
mixedScopeLayouts?- Type
'auto' | string[]- Description
Layouts and templates that intentionally wrap both protected and public descendants. Defaults to
'auto', which skips these files silently. Set this option to an array to require each mixed-scope layout or template folder to be acknowledged explicitly. This makes the rule warn when it detects new unacknowledged mixed-scope layouts, for example when adding a new public descendant to a previously protected folder.
- Name
rootDir?- Type
string- Description
Project root used to resolve project-relative folder globs. Defaults to the nearest ancestor
eslint.config.*file, then ESLint's current working directory. Set this toimport.meta.dirnameor to an application directory when config auto-discovery doesn't match your project structure. Relative paths are resolved againstcwd.
How the rule works
What the rule checks
You tell the rule which folders should require authentication (protected) and which are exempt (public); the rule then verifies that every server-side resource inside a protected folder actually performs that check. It runs at lint time only — it doesn't enforce anything at runtime, and it doesn't check .
Within folders configured as protected, the rule checks the following resources when their resource group is enabled:
serverComponentEntrypoints: Default exports frompage,layout,template, anddefaultfiles.routeHandlers: HTTP method exports fromroutefiles, such asGET,POST,PUT, andDELETE.serverFunctions: All exports from files with'use server'at the top, and all inline functions with'use server'at the top of the function body.
The rule doesn't check:
- Files with
'use client'at the top. loadingorerrorfiles.- Arbitrary Server Components that are imported into pages.
- External API endpoints called by your application.
Client Components aren't checked because they can't reach privileged data directly — they go through Server Functions or Route Handlers, which the rule checks separately. Protect the resources they receive as props and the Route Handlers or external API endpoints they call instead.
What counts as an authentication check
The rule accepts a narrow set of authentication checks. The most direct pattern is auth.protect()
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
// Redirects unauthenticated users to the sign-in page.
await auth.protect()
// Passing an authorization check, such as a Role or Permission, also works.
await auth.protect({ role: 'org:admin' })
}It also accepts authentication checks derived from auth()auth() destructure must be immediately followed by the authentication check.
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
const { isAuthenticated, redirectToSignIn } = await auth()
if (!isAuthenticated) return redirectToSignIn()
// ...
}Wrapped or imported handlers
The rule doesn't follow protection across files or through wrapper calls. For example, it can't verify that a handler is protected when you re-export it from another file, or when you wrap it with a helper such as withAuth(handler).
If the resource is already protected by code the rule can't inspect, add an ESLint disable comment with a reason:
// eslint-disable-next-line @clerk/next/require-auth-protection -- Protected by withAuth().
export const POST = withAuth(handler)For imported handlers, either add a local wrapper that calls await auth.protect(), or disable the rule after verifying that the imported handler protects itself:
// eslint-disable-next-line @clerk/next/require-auth-protection -- `handler` calls auth.protect().
export { handler as GET } from './handler'Editor suggestions
When the rule finds an unprotected resource, it offers an editor quick-fix suggestion that inserts await auth.protect() at the top of the function. When needed, the suggestion also makes the function async and adds import { auth } from '@clerk/nextjs/server'.
These suggestions are opt-in. They appear in your editor's quick-fix menu and aren't applied by eslint --fix, because adding an authentication check changes runtime behavior.
Bulk Fixer CLI
The package includes the Bulk Fixer CLI for migrations or large cleanups.
The Bulk Fixer CLI runs ESLint with your existing config, respects your protected, public, and resources options, and applies the rule's await auth.protect() suggestion anywhere it can do so safely.
Run the fixer with npx:
# Fix everything under the current directory.
npx clerk-next-fix-auth-protection
# Preview without writing files.
npx clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
npx clerk-next-fix-auth-protection "src/**"# Fix everything under the current directory.
pnpm dlx clerk-next-fix-auth-protection
# Preview without writing files.
pnpm dlx clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
pnpm dlx clerk-next-fix-auth-protection "src/**"# Fix everything under the current directory.
yarn dlx clerk-next-fix-auth-protection
# Preview without writing files.
yarn dlx clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
yarn dlx clerk-next-fix-auth-protection "src/**"# Fix everything under the current directory.
bun x clerk-next-fix-auth-protection
# Preview without writing files.
bun x clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
bun x clerk-next-fix-auth-protection "src/**"The command exits with a non-zero status when:
--dry-runreports changes that would be made.- Some resources need manual attention.
The following examples demonstrate the resource-based checks that the Bulk Fixer CLI inserts.
When a user isn't authenticated, auth.protect()401 for Server Actions, and returns a 404 for other non-document requests (such as Route Handlers).
If you want more control over the response, or if you want to perform instead, adjust resources manually.
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
await auth.protect()
return <h1>Dashboard</h1>
}import { auth } from '@clerk/nextjs/server'
export async function GET() {
await auth.protect()
return Response.json({ ok: true })
}'use server'
import { auth } from '@clerk/nextjs/server'
export async function updateDashboard() {
await auth.protect()
// Add your Server Function logic
}The Bulk Fixer CLI will warn about resources that require manual attention, such as imported or wrapped exports, or mixed-scope layouts that require explicit acknowledgement. Add protection manually to those resources when they still need it.
If the resource is already protected by a wrapper or by code in another file, the rule can't currently verify that protection. In that case, add an ESLint disable comment with a reason:
// eslint-disable-next-line @clerk/next/require-auth-protection -- Protected by withAuth().
export const POST = withAuth(handler)clerk-next-fix-auth-protection [patterns...] [options]- Name
patterns- Type
string[]- Description
Files, directories, or globs to scan. Defaults to
['.'].
- Name
--dry-run- Type
boolean- Description
Reports what would change without writing files.
- Name
-h,--help- Type
boolean- Description
Shows command line help.
Programmatic API
You can also run the Bulk Fixer programmatically from a Node script:
import { fixAuthProtection } from '@clerk/eslint-plugin/next/fix-auth-protection'
const { fixed, unresolved } = await fixAuthProtection({
patterns: ['src/**'],
dryRun: false,
})fixAuthProtection() accepts the following options:
- Name
patterns?- Type
string[]- Description
Files, directories, or globs to scan. Defaults to
['.'].
- Name
cwd?- Type
string- Description
Working directory that ESLint resolves config and files against. Defaults to
process.cwd().
- Name
dryRun?- Type
boolean- Description
Computes changes without writing them to disk.
- Name
eslint?- Type
ESLint- Description
Advanced option for passing a preconfigured ESLint instance. If omitted, the Bulk Fixer CLI creates an ESLint instance using the provided
cwd.
- Name
onConfigResolved?- Type
(configFilePath: string | undefined) => void- Description
Called before scanning with the ESLint config file path that will be used.
- Name
onScanComplete?- Type
(fileCount: number) => void- Description
Called after linting finishes and before per-file fixing begins.
- Name
onFileFixed?- Type
(file: FixedFile) => void- Description
Called after each file is fixed, or after each file that would be fixed in dry-run mode.
fixAuthProtection() returns:
- Name
fixed- Type
{ filePath: string, protections: number }[]- Description
Files that were modified, or that would be modified in dry-run mode.
- Name
unresolved- Type
{ filePath: string, issues: { line: number, column: number, message: string }[] }[]- Description
Files with flagged resources that have no safe automatic fix.
Feedback
Last updated on