{"record":{"id":"a95bbd68bce59115","repo":"apple/pkl","slug":"output-file-conflict-output-files-entry-path-a95bbd","errorCode":null,"errorMessage":"Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$realOutputDir`.","messagePattern":"Output file conflict: `output\\.files` entry `\"\\$pathSpec\"` in module `\\$moduleUri` resolves to file path `\\$realPath`, which is outside output directory `\\$realOutputDir`\\.","errorType":"exception","errorClass":"CliException","httpStatus":null,"severity":"error","filePath":"pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt","lineNumber":245,"sourceCode":"   * directory.\n   */\n  private fun writeMultipleFileOutput(builder: EvaluatorBuilder) {\n    val outputDirs = directoryOutputPaths!!\n    val writtenFiles = mutableMapOf<Path, OutputFile>()\n    builder.setOutputFormat(options.outputFormat).build().use { evaluator ->\n      for ((moduleUri, outputDir) in outputDirs) {\n        if (outputDir.exists() && !outputDir.isDirectory()) {\n          throw CliException(\"Output path `$outputDir` exists and is not a directory.\")\n        }\n        val moduleSource = toModuleSource(moduleUri, inputStream)\n        val output = evaluator.evaluateOutputFiles(moduleSource)\n        val realOutputDir = if (outputDir.exists()) outputDir.toRealPath() else outputDir\n\n        for ((pathSpec, fileOutput) in output) {\n          checkPathSpec(pathSpec)\n          val (realPath, resolvedPath) = realOutputDir.resolveRealPath(Path.of(pathSpec))\n          if (!realPath.startsWith(realOutputDir)) {\n            throw CliException(\n              \"Output file conflict: `output.files` entry `\\\"$pathSpec\\\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$realOutputDir`.\"\n            )\n          }\n          val previousOutput = writtenFiles[realPath]\n          if (previousOutput != null) {\n            throw CliException(\n              \"Output file conflict: `output.files` entries `\\\"${previousOutput.pathSpec}\\\"` in module `${previousOutput.moduleUri}` and `\\\"$pathSpec\\\"` in module `$moduleUri` resolve to the same file path `$realPath`.\"\n            )\n          }\n          if (realPath.isDirectory()) {\n            throw CliException(\n              \"Output file conflict: `output.files` entry `\\\"$pathSpec\\\"` in module `$moduleUri` resolves to file path `$realPath`, which is a directory.\"\n            )\n          }\n          writtenFiles[realPath] = OutputFile(pathSpec, moduleUri)\n          realPath.createParentDirectories()\n          realPath.writeBytes(fileOutput.bytes)\n          outputStream.writeText(","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/apple/pkl/blob/f3efcbfc9b60d30053b0536d664948d7aa1b8673/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt#L227-L263","documentation":"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.","triggerScenarios":"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.","commonSituations":"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 `..`.","solutions":["Rewrite the offending `output.files` path spec so it stays inside the output directory (remove `..` segments).","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.","Resolve symlinks or restructure the project so outputs and the output directory share a real (non-symlinked) path."],"exampleFix":"// before\noutput.files {\n  [\"../dist/config.json\"]\n}\n// after (run with --output-dir pointing at project root)\noutput.files {\n  [\"dist/config.json\"]\n}","handlingStrategy":"validation","validationCode":"val outDir = File(\"dist\").canonicalFile\nval target = File(\"dist/../shared/out.json\").canonicalFile\nrequire(target.toPath().startsWith(outDir.toPath())) { \"output path escapes output dir\" }","typeGuard":"fun isInsideOutputDir(pathSpec: String, outputDir: Path): Boolean =\n  outputDir.resolveRealPath(pathSpec).realPath.startsWith(outputDir)","tryCatchPattern":"try {\n  pklCli.runEval(args)\n} catch (e: CliException) {\n  if (\"outside output directory\" in e.message ?: \"\") fixPathSpec(e)\n  else throw e\n}","preventionTips":["Use output paths relative to and inside the output directory; never `..`.","Avoid symlinks inside output directories.","Verify path specs in CI with a pre-check script."],"tags":["cli","path-traversal","file-output"],"backgroundTag":"path-traversal-blocked","analyzedSha":"f3efcbfc9b60d30053b0536d664948d7aa1b8673","analyzedAt":"2026-09-08T13:10:45.570Z","contentChangedAt":"2026-09-08T13:10:45.570Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}