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
- 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.
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
- 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.
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
- Cannot download packages because no cache directory is…
- Cannot generate documentation for just one module within a…
- Cannot generate JUnit report for $moduleUri. A report with…
- Cannot substitute output path placeholder
- Cannot write to stdin
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)