Skip to content

Conversation

@cderv
Copy link
Collaborator

@cderv cderv commented Jan 15, 2026

When _quarto.yml contains output-dir: ./, running quarto render would delete the entire project directory, leaving only an empty .quarto/ directory.

Root Cause

Two compounding issues in the output directory handling:

  1. The special case in project-context.ts:306-308 only checked for exact string ".":
if (projectConfig.project[kProjectOutputDir] === ".") {
  delete projectConfig.project[kProjectOutputDir];
}

This missed "./" and other equivalent variations.

  1. The deletion logic in project.ts:287-291 used string comparison:
if ((realOutputDir !== realProjectDir) &&
    realOutputDir.startsWith(realProjectDir)) {
  removeIfExists(realOutputDir);
}

After path normalization, . and ./ produce different strings (e.g., C:\project vs C:\project\), causing the safety check to incorrectly pass.

Why resolve() instead of normalize()

We verified empirically that normalize() doesn't work for this case:

  • normalize("./").\ on Windows (keeps trailing separator)
  • normalize(".").

But resolve() correctly handles all variations:

  • resolve(dir, ".") and resolve(dir, "./") both → same absolute path
  • This works consistently across platforms

Fix Approach

  1. Use resolve() for path comparison - handles all current-directory variations (., ./, .\, ././, etc.)

  2. Replace removeIfExists with safeRemoveDirSync as defense in depth - this function uses proper isSubdir boundary checking and throws UnsafeRemovalError if someone attempts to delete the project directory

Test Plan

  • Unit test verifying resolve() behavior for path equivalence (platform-aware: .\ only tested on Windows)
  • Smoke tests for output-dir: ./, output-dir: ., and output-dir: _output
  • All tests verify marker files survive rendering

Fixes #13892

When _quarto.yml contained `output-dir: ./`, running `quarto render` would
delete the entire project directory. This happened because:

1. The special case handling only checked for `output-dir: "."` but not `"./"`
2. Path normalization produced different strings for `.` vs `./` on Windows,
   causing the safety check to fail

Root Cause:
- `normalize("./")` produces `.\` on Windows (not `.`)
- String comparison `realOutputDir !== realProjectDir` would pass due to
  trailing separator difference, allowing deletion to proceed

Fix:
- Use `resolve()` for path comparison which correctly handles all variations
  (`.`, `./`, `.\`, `././`, etc.) by resolving to absolute paths
- Replace unsafe `removeIfExists` with `safeRemoveDirSync` which uses proper
  `isSubdir` boundary checking as defense in depth

Fixes #13892

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@posit-snyk-bot
Copy link
Collaborator

posit-snyk-bot commented Jan 15, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cderv cderv marked this pull request as ready for review January 15, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

setting output-dir: ./ will rm -rf the entire project

3 participants