Repository: vercel/next.js
Pull request: [create-next-app] Skip interactive prompts when CLI flags are provided

When AI agents run `create-next-app` with explicit flags like `--typescript --tailwind --eslint --app --src-dir`, the CLI still enters interactive mode and prompts for any unspecified options:

```
➜ npx create-next-app my-app --typescript --tailwind --eslint --app --src-dir --use-pnpm
✔ Would you like to use React Compiler? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? › No / Yes
```

Agents can sometimes answer these interactive prompts correctly, but often they can't. When they fail to navigate the prompts, they fall back to scaffolding the project themselves from scratch — generating files based on stale training data. This means they might initialize an app using deprecated patterns or pin to an older version of Next.js (e.g. 15) instead of the latest. Using `create-next-app` non-interactively is the best way to ensure agents always produce up-to-date scaffolding.

Previously, `hasProvidedOptions` only skipped the initial "use recommended defaults?" meta-prompt but still showed individual prompts for each missing option. Now when any config flags are provided, all remaining options use the recommended defaults without prompting.

The resolved defaults are printed to stdout so the caller knows exactly what was assumed and which flags to pass to override:

```
Using defaults for unprovided options:

  --eslint                ESLint (use --biome for Biome, --no-eslint for None)
  --no-react-compiler     No React Compiler (use --react-compiler for React Compiler)
  --agents-md             AGENTS.md (use --no-agents-md for No AGENTS.md)
  --import-alias          "@/*"

To customize, re-run with explicit flags.
```

The `displayConfig` array is extended with a `flags` field so this output is auto-generated from the same source of truth used for the interactive prompts. Existing behavior for `--yes`, CI mode, and fully interactive mode (no flags) is unchanged.

Changed files:
filename: packages/create-next-app/index.ts
patch: @@ -251,19 +251,45 @@ async function run(): Promise<void> {
     type DisplayConfigItem = {
       key: keyof typeof defaults
       values?: Record<string, string>
+      flags?: Record<string, string>
     }
 
     const displayConfig: DisplayConfigItem[] = [
       {
         key: 'typescript',
         values: { true: 'TypeScript', false: 'JavaScript' },
+        flags: { true: '--ts', false: '--js' },
+      },
+      {
+        key: 'linter',
+        values: { eslint: 'ESLint', biome: 'Biome', none: 'None' },
+        flags: { eslint: '--eslint', biome: '--biome', none: '--no-eslint' },
+      },
+      {
+        key: 'reactCompiler',
+        values: { true: 'React Compiler', false: 'No React Compiler' },
+        flags: { true: '--react-compiler', false: '--no-react-compiler' },
+      },
+      {
+        key: 'tailwind',
+        values: { true: 'Tailwind CSS', false: 'No Tailwind CSS' },
+        flags: { true: '--tailwind', false: '--no-tailwind' },
+      },
+      {
+        key: 'srcDir',
+        values: { true: 'src/ directory', false: 'No src/ directory' },
+        flags: { true: '--src-dir', false: '--no-src-dir' },
+      },
+      {
+        key: 'app',
+        values: { true: 'App Router', false: 'Pages Router' },
+        flags: { true: '--app', false: '--no-app' },
+      },
+      {
+        key: 'agentsMd',
+        values: { true: 'AGENTS.md', false: 'No AGENTS.md' },
+        flags: { true: '--agents-md', false: '--no-agents-md' },
       },
-      { key: 'linter', values: { eslint: 'ESLint', biome: 'Biome' } },
-      { key: 'reactCompiler', values: { true: 'React Compiler' } },
-      { key: 'tailwind', values: { true: 'Tailwind CSS' } },
-      { key: 'srcDir', values: { true: 'src/ dir' } },
-      { key: 'app', values: { true: 'App Router', false: 'Pages Router' } },
-      { key: 'agentsMd', values: { true: 'AGENTS.md' } },
     ]
 
     // Helper to format settings for display based on displayConfig
@@ -291,10 +317,17 @@ async function run(): Promise<void> {
     const hasSavedPreferences = Object.keys(preferences).length > 0
 
     // Check if user provided any configuration flags
-    // If they did, skip the "recommended defaults" prompt and go straight to
-    // individual prompts for any missing options
+    // If they did, skip all prompts and use recommended defaults for unspecified
+    // options. This is critical for AI agents, which pass flags like
+    // --typescript --tailwind --app and expect the rest to use sensible defaults
+    // without entering interactive mode.
     const hasProvidedOptions = process.argv.some((arg) => arg.startsWith('--'))
 
+    if (!skipPrompt && hasProvidedOptions) {
+      skipPrompt = true
+      useRecommendedDefaults = true
+    }
+
     // Only show the "recommended defaults" prompt if:
     // - Not in CI and not using --yes flag
     // - User hasn't provided any custom options
@@ -620,6 +653,56 @@ async function run(): Promise<void> {
         preferences.agentsMd = Boolean(agentsMd)
       }
     }
+
+    // When prompts were skipped because flags were provided, print the
+    // defaults that were assumed so agents and users know what to override.
+    if (hasProvidedOptions && useRecommendedDefaults) {
+      const lines: string[] = []
+
+      for (const config of displayConfig) {
+        if (!config.flags || !config.values) continue
+
+        // Skip options the user already specified explicitly
+        const wasExplicit = process.argv.some((arg) =>
+          Object.values(config.flags!).includes(arg)
+        )
+        if (wasExplicit) continue
+
+        const value = String(defaults[config.key])
+        const flag = config.flags[value]
+        const label = config.values[value]
+        if (!flag || !label) continue
+
+        // Show alternatives the user could pass instead
+        const alts: string[] = []
+        for (const [k, f] of Object.entries(config.flags)) {
+          if (k !== value && config.values[k]) {
+            alts.push(`${f} for ${config.values[k]}`)
+          }
+        }
+
+        const altText = alts.length > 0 ? ` (use ${alts.join(', ')})` : ''
+        lines.push(`  ${flag.padEnd(24)}${label}${altText}`)
+      }
+
+      // Import alias is not a boolean toggle, handle separately
+      const hasImportAlias = process.argv.some(
+        (arg) =>
+          arg.startsWith('--import-alias') ||
+          arg.startsWith('--no-import-alias')
+      )
+      if (!hasImportAlias) {
+        lines.push(`  ${'--import-alias'.padEnd(24)}"${defaults.importAlias}"`)
+      }
+
+      if (lines.length > 0) {
+        console.log(
+          '\nUsing defaults for unprovided options:\n\n' +
+            lines.join('\n') +
+            '\n'
+        )
+      }
+    }
   }
 
   const bundler: Bundler = opts.rspack ? Bundler.Rspack : Bundler.Turbopack

filename: test/integration/create-next-app/examples.test.ts
patch: @@ -233,8 +233,12 @@ describe('create-next-app --example', () => {
         [
           projectName,
           '--js',
+          '--no-app',
           '--no-tailwind',
           '--eslint',
+          '--no-src-dir',
+          '--no-react-compiler',
+          '--no-agents-md',
           '--example',
           'default',
           '--import-alias=@/*',

filename: test/integration/create-next-app/index.test.ts
patch: @@ -164,6 +164,88 @@ describe('create-next-app', () => {
     })
   })
 
+  it('should print assumed defaults when flags are partially provided', async () => {
+    await useTempDir(async (cwd) => {
+      const projectName = 'partial-flags'
+
+      const res = await run(
+        [
+          projectName,
+          '--ts',
+          '--tailwind',
+          '--app',
+          '--skip-install',
+          ...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
+        ],
+        nextTgzFilename,
+        {
+          cwd,
+          stdio: 'pipe',
+        }
+      )
+      expect(res.exitCode).toBe(0)
+
+      // Extract the defaults block from stdout
+      const defaultsMatch = res.stdout.match(
+        /Using defaults for unprovided options:\n\n([\s\S]*?)\n\nCreating/
+      )
+      expect(defaultsMatch).not.toBeNull()
+      expect(defaultsMatch[1]).toMatchInlineSnapshot(`
+        "  --eslint                ESLint (use --biome for Biome, --no-eslint for None)
+          --no-react-compiler     No React Compiler (use --react-compiler for React Compiler)
+          --no-src-dir            No src/ directory (use --src-dir for src/ directory)
+          --agents-md             AGENTS.md (use --no-agents-md for No AGENTS.md)
+          --import-alias          "@/*""
+      `)
+    })
+  })
+
+  it('should not print assumed defaults when all flags are provided', async () => {
+    await useTempDir(async (cwd) => {
+      const projectName = 'all-flags'
+
+      const res = await run(
+        [
+          projectName,
+          '--ts',
+          '--app',
+          '--eslint',
+          '--tailwind',
+          '--no-src-dir',
+          '--no-import-alias',
+          '--no-react-compiler',
+          '--no-agents-md',
+          '--skip-install',
+          ...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
+        ],
+        nextTgzFilename,
+        {
+          cwd,
+          stdio: 'pipe',
+        }
+      )
+      expect(res.exitCode).toBe(0)
+      expect(res.stdout).not.toContain('Using defaults for unprovided options')
+    })
+  })
+
+  it('should not print assumed defaults with --yes flag', async () => {
+    await useTempDir(async (cwd) => {
+      const projectName = 'yes-flag'
+
+      const res = await run(
+        [projectName, '--yes', '--skip-install'],
+        nextTgzFilename,
+        {
+          cwd,
+          stdio: 'pipe',
+        }
+      )
+      expect(res.exitCode).toBe(0)
+      expect(res.stdout).not.toContain('Using defaults for unprovided options')
+    })
+  })
+
   it('should not install dependencies if --skip-install', async () => {
     await useTempDir(async (cwd) => {
       const projectName = 'empty-dir'

filename: test/integration/create-next-app/prompts.test.ts
patch: @@ -57,7 +57,7 @@ describe('create-next-app prompts', () => {
     })
   })
 
-  it('should prompt user for choice if --js or --ts flag is absent', async () => {
+  it('should use default for --ts when other flags are provided', async () => {
     await useTempDir(async (cwd) => {
       const projectName = 'ts-js'
       const childProcess = createNextApp(
@@ -77,24 +77,23 @@ describe('create-next-app prompts', () => {
         nextTgzFilename
       )
 
+      // No stdin interaction needed - defaults are used automatically
       await new Promise<void>((resolve) => {
         childProcess.on('exit', async (exitCode) => {
           expect(exitCode).toBe(0)
+          // Default is TypeScript
           projectFilesShouldExist({
             cwd,
             projectName,
             files: ['tsconfig.json'],
           })
           resolve()
         })
-
-        // select default choice: typescript
-        childProcess.stdin.write('\n')
       })
     })
   })
 
-  it('should prompt user for choice if --tailwind is absent', async () => {
+  it('should use default for --tailwind when other flags are provided', async () => {
     await useTempDir(async (cwd) => {
       const projectName = 'tw'
       const childProcess = createNextApp(
@@ -114,24 +113,23 @@ describe('create-next-app prompts', () => {
         nextTgzFilename
       )
 
+      // No stdin interaction needed - defaults are used automatically
       await new Promise<void>((resolve) => {
         childProcess.on('exit', async (exitCode) => {
           expect(exitCode).toBe(0)
+          // Default is Tailwind enabled
           projectFilesShouldExist({
             cwd,
             projectName,
             files: ['postcss.config.mjs'],
           })
           resolve()
         })
-
-        // select default choice: tailwind
-        childProcess.stdin.write('\n')
       })
     })
   })
 
-  it('should prompt user for choice if --import-alias is absent', async () => {
+  it('should use default import alias when other flags are provided', async () => {
     await useTempDir(async (cwd) => {
       const projectName = 'import-alias'
       const childProcess = createNextApp(
@@ -151,27 +149,18 @@ describe('create-next-app prompts', () => {
         nextTgzFilename
       )
 
-      await new Promise<void>(async (resolve) => {
+      // No stdin interaction needed - default import alias @/* is used
+      await new Promise<void>((resolve) => {
         childProcess.on('exit', async (exitCode) => {
           expect(exitCode).toBe(0)
           resolve()
         })
-        let output = ''
-        childProcess.stdout.on('data', (data) => {
-          output += data
-          process.stdout.write(data)
-        })
-        // cursor forward, choose 'Yes' for custom import alias
-        childProcess.stdin.write('\u001b[C\n')
-        // used check here since it needs to wait for the prompt
-        await check(() => output, /What import alias would you like configured/)
-        childProcess.stdin.write('@/something/*\n')
       })
 
       const tsConfig = require(join(cwd, projectName, 'tsconfig.json'))
       expect(tsConfig.compilerOptions.paths).toMatchInlineSnapshot(`
         {
-          "@/something/*": [
+          "@/*": [
             "./*",
           ],
         }