vercel/turborepo · error · Error

Found both turbo.json and turbo.jsonc in the same directory:

Error message

Found both turbo.json and turbo.jsonc in the same directory: ${dir}
Please use either turbo.json or turbo.jsonc, but not both.

What it means

getTurboConfigs() globs the monorepo for turbo.json/turbo.jsonc, groups matches per directory, and throws when a single directory contains more than one config file. Turborepo cannot decide which file wins, so it refuses rather than guessing, and the message names the offending directory.

Source

Thrown at packages/turbo-utils/src/get-turbo-configs.ts:196

    const configPathsByDir: Record<string, Array<string>> = {};

    // Group config paths by directory
    for (const configPath of configPaths) {
      const dir = path.dirname(configPath);
      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- configPathsByDir[dir] can be undefined
      if (!configPathsByDir[dir]) {
        configPathsByDir[dir] = [];
      }
      configPathsByDir[dir].push(configPath);
    }

    // Process each directory
    for (const [dir, dirConfigPaths] of Object.entries(configPathsByDir)) {
      // If both turbo.json and turbo.jsonc exist in the same directory, throw an error
      if (dirConfigPaths.length > 1) {
        const errorMessage = `Found both turbo.json and turbo.jsonc in the same directory: ${dir}\nPlease use either turbo.json or turbo.jsonc, but not both.`;
        logger.error(errorMessage);
        throw new Error(errorMessage);
      }

      const configPath = dirConfigPaths[0];
      try {
        const raw = fs.readFileSync(configPath, "utf8");

        const turboJsonContent: SchemaV1 = JSON5.parse(raw);
        // basic config validation
        const isRootConfig = path.dirname(configPath) === turboRoot;
        if (isRootConfig) {
          // invalid - root config with extends
          if ("extends" in turboJsonContent) {
            continue;
          }
        } else if (!("extends" in turboJsonContent)) {
          // invalid - workspace config with no extends
          continue;
        }

View on GitHub (pinned to 9f94a7d215)

Solutions

  1. In the directory named in the message, delete (git rm) either turbo.json or turbo.jsonc so exactly one remains
  2. If both contain changes, merge their contents into the surviving file first
  3. Sweep for other duplicates: find . -name 'turbo.json*' -exec dirname {} \; | sort | uniq -d

Example fix

# before: both files present in apps/web
apps/web/turbo.json
apps/web/turbo.jsonc

# after: keep exactly one
git rm apps/web/turbo.json
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from "node:fs";
import path from "node:path";

function hasConflictingTurboConfig(dir: string): boolean {
  return existsSync(path.join(dir, "turbo.json")) && existsSync(path.join(dir, "turbo.jsonc"));
}

for (const dir of workspaceDirs) {
  if (hasConflictingTurboConfig(dir)) throw new Error(`Both turbo.json and turbo.jsonc in ${dir}`);
}
await getTurboConfigs(turboRoot);

Try / catch

try {
  getTurboConfigs(turboRoot);
} catch (e) {
  if (e instanceof Error && e.message.includes("Found both turbo.json and turbo.jsonc")) {
    // parse the directory from the message, prompt deletion of one file, then retry
  } else throw e;
}

Prevention

When it happens

Trigger: Any workspace directory (including the root) containing both turbo.json and turbo.jsonc while getTurboConfigs()/getTurboRoot() runs — e.g. after adding the .jsonc variant without deleting the original, or a merge resurrecting one file.

Common situations: Half-finished migration from turbo.json to turbo.jsonc; merge conflicts reintroducing the deleted file; codegen writing turbo.json into a workspace someone hand-migrated to .jsonc.

Related errors


AI-assisted analysis of vercel/turborepo@9f94a7d215 (2026-08-16). Data as JSON: /api/errors/38176d3865c80a22. Report an issue: GitHub.