apple/pkl · error · CliException

Output file conflict: `output.files` entry `"$pathSpec"` in…

Error message

Output file conflict: `output.files` entry `"$pathSpec"` in module `$moduleUri` resolves to file path `$realPath`, which is a directory.

What it means

An `output.files` entry resolves to a path that currently is a directory on disk, so the CLI cannot write bytes to it and throws from writeMultipleFileOutput. This happens after the outside-directory and duplicate-path checks pass.

Solutions

  1. Change the path spec to include a filename inside the directory, e.g. `build/output.json`.
  2. Remove or rename the existing directory at the resolved path if it is stale.
  3. Check the resolved path printed in the message to confirm what the CLI attempted to write.

Example fix

// before
output.files { ["build"] }
// after
output.files { ["build/output.json"] }
Defensive patterns

Strategy: validation

Validate before calling

val target = outDir.resolveRealPath(pathSpec).realPath
check(!java.nio.file.Files.isDirectory(target)) { "$pathSpec resolves to a directory" }

Try / catch

try {
  pklCli.runEval(args)
} catch (e: CliException) {
  if ("which is a directory" in (e.message ?: "")) fixPathToFileName(e)
  else throw e
}

Prevention

When it happens

Trigger: Path spec `foo` when `foo/` exists as a directory in the output directory, or a path spec that omits the filename while a directory of that name exists (e.g. `output.files { ["build"] }`).

Common situations: After refactoring, an output previously written to `build/out.json` becomes `build`, which is now a directory; or users mistake the files map value for a directory target instead of a file path.

Related errors


AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08). Data as JSON: /api/errors/39998499dfb7e743. Report an issue: GitHub.

Appendix: source

Thrown at pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt:256

        val output = evaluator.evaluateOutputFiles(moduleSource)
        val realOutputDir = if (outputDir.exists()) outputDir.toRealPath() else outputDir

        for ((pathSpec, fileOutput) in output) {
          checkPathSpec(pathSpec)
          val (realPath, resolvedPath) = realOutputDir.resolveRealPath(Path.of(pathSpec))
          if (!realPath.startsWith(realOutputDir)) {
            throw CliException(
              "Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$realOutputDir`."
            )
          }
          val previousOutput = writtenFiles[realPath]
          if (previousOutput != null) {
            throw CliException(
              "Output file conflict: `output.files` entries `\"${previousOutput.pathSpec}\"` in module `${previousOutput.moduleUri}` and `\"$pathSpec\"` in module `$moduleUri` resolve to the same file path `$realPath`."
            )
          }
          if (realPath.isDirectory()) {
            throw CliException(
              "Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is a directory."
            )
          }
          writtenFiles[realPath] = OutputFile(pathSpec, moduleUri)
          realPath.createParentDirectories()
          realPath.writeBytes(fileOutput.bytes)
          outputStream.writeText(
            IoUtils.relativize(resolvedPath, currentWorkingDir).toString() +
              IoUtils.getLineSeparator()
          )
          outputStream.flush()
        }
      }
    }
  }

  /**
   * Resolves [rel] against this Path name-by-name. At each step, the real path is resolved if the

View on GitHub (pinned to f3efcbfc9b)