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 outside output directory `$realOutputDir`.

What it means

The Pkl CLI refuses to write a file declared in `output.files` when its path spec resolves outside the output directory (e.g. via `..` segments or symlinks escaping the real output dir). This guard prevents a Pkl module from overwriting arbitrary files on disk. It is thrown from CliEvaluator.writeMultipleFileOutput after resolving the path against the real output directory.

Solutions

  1. Rewrite the offending `output.files` path spec so it stays inside the output directory (remove `..` segments).
  2. Run the CLI with an output directory that is the common parent, e.g. `--output-dir ..` or the parent project dir, so the intended path is inside it.
  3. Resolve symlinks or restructure the project so outputs and the output directory share a real (non-symlinked) path.

Example fix

// before
output.files {
  ["../dist/config.json"]
}
// after (run with --output-dir pointing at project root)
output.files {
  ["dist/config.json"]
}
Defensive patterns

Strategy: validation

Validate before calling

val outDir = File("dist").canonicalFile
val target = File("dist/../shared/out.json").canonicalFile
require(target.toPath().startsWith(outDir.toPath())) { "output path escapes output dir" }

Type guard

fun isInsideOutputDir(pathSpec: String, outputDir: Path): Boolean =
  outputDir.resolveRealPath(pathSpec).realPath.startsWith(outputDir)

Try / catch

try {
  pklCli.runEval(args)
} catch (e: CliException) {
  if ("outside output directory" in e.message ?: "") fixPathSpec(e)
  else throw e
}

Prevention

When it happens

Trigger: Running `pkl eval -f json ... -x` or any evaluation whose output.files entry contains a path like `../foo.txt`, an absolute path, or a symlinked path that normalizes to a location not starting with realOutputDir.

Common situations: Projects that render outputs into sibling directories (`../shared/output.pkl`), symlinked output directories, or PklProject setups where the output directory is relocated but path specs still contain `..`.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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

Appendix: source

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

   * directory.
   */
  private fun writeMultipleFileOutput(builder: EvaluatorBuilder) {
    val outputDirs = directoryOutputPaths!!
    val writtenFiles = mutableMapOf<Path, OutputFile>()
    builder.setOutputFormat(options.outputFormat).build().use { evaluator ->
      for ((moduleUri, outputDir) in outputDirs) {
        if (outputDir.exists() && !outputDir.isDirectory()) {
          throw CliException("Output path `$outputDir` exists and is not a directory.")
        }
        val moduleSource = toModuleSource(moduleUri, inputStream)
        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(

View on GitHub (pinned to f3efcbfc9b)