Skip to content

fix(cli): refuse to scaffold into a non-empty directory - #493

Merged
blurrah merged 1 commit into
blurra/inline-skills-scaffoldfrom
cli-non-empty-dir-guard
Aug 7, 2026
Merged

fix(cli): refuse to scaffold into a non-empty directory#493
blurrah merged 1 commit into
blurra/inline-skills-scaffoldfrom
cli-non-empty-dir-guard

Conversation

@blurrah

@blurrah blurrah commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes the security finding from the adversarial review of #492: the CLI scaffolding can overwrite files in an existing directory.

Targets blurra/inline-skills-scaffold so #492 ships with the fix — the finding cites that branch's apps/cli/index.mjs:L214-L286.

Problem

main() resolved the user-supplied name to projectDir, created it unconditionally with mkdir({ recursive: true }), then copied the downloaded template in with fs.cp(..., { recursive: true }). Nothing checked that the target was empty and nothing asked for confirmation, so pointing the CLI at an existing project silently overwrote same-named files — before <pm> install and git init ran against it.

Solution

Inspect the target before scaffolding:

  • Empty or missing — proceeds exactly as before.
  • Non-empty, TTY — asks … already contains <entries>. Scaffolding may overwrite existing files. Continue? (y/N), defaulting to no. Declining exits 1 without writing anything.
  • Non-empty, non-interactive (CI, coding agent) — exits 1 with the conflicting entries listed and no confirmation prompt.
  • --force — new flag that opts back in, with a warning naming the directory.
  • --no-template — exempt. Adding agent assets to an existing project is the whole point of that mode.

A target path that exists but is not a directory now reports a clear error instead of crashing on the later mkdir.

Every entry counts, including dotfiles — no ignore list. A strict rule with an explicit escape hatch is easier to audit than one that guesses which existing files are safe to clobber.

Tests

apps/cli/index.test.mjs grows 10 cases (19 total, all passing): the refusal path leaves existing files byte-identical and runs no install/git subprocesses; dotfile-only directories count as non-empty; empty-but-existing directories scaffold without prompting; the TTY confirmation is honored in both directions; --force never prompts; --no-template still works against a populated project; and readTargetEntries reports a missing path as empty but rejects a file.

Also verified end-to-end against the real binary — it refuses a populated directory with exit 1 and leaves it untouched.

Docs

  • Troubleshooting entry for the new "is not empty" stop.
  • init-vercel-shop skill now tells agents the CLI enforces its existing "don't overwrite" step and to only reach for --force on explicit user confirmation. (No content/docs/skills/init-vercel-shop.mdx mirror exists, so no docs skill sync is needed.)

Note

main has the same gap in its tar-based fetchTemplate. This guard sits in main(), ahead of the scaffold call, so it covers that path too once #492 lands.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
shop-docs Ready Ready Preview Aug 7, 2026 3:14pm
shop-template Ready Ready Preview, v0 Aug 7, 2026 3:14pm

The CLI resolved the user-supplied name to projectDir, created it with
mkdir({ recursive: true }), and copied the template in with
fs.cp(..., { recursive: true }). Nothing checked that the target was
empty, so pointing it at an existing project silently overwrote
same-named files before install and git init ran.

Inspect the target before scaffolding. Empty or missing proceeds as
before; non-empty prompts for confirmation on a TTY (default no) and
exits 1 in non-interactive environments like CI or a coding agent. The
new --force flag opts back in. --no-template is exempt, since adding
agent assets to an existing project is the point of that mode. A target
that exists but is not a directory now reports the error instead of
crashing on the later mkdir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Co-Authored-By: Boris Besemer <2852507+blurrah@users.noreply.github.com>

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

On Windows, the .claude/skills/<name> junction is created with a relative target that Node resolves against process.cwd() instead of the link's directory, producing a silent dangling junction.

Fix on Vercel

@blurrah
blurrah merged commit 03b2d60 into blurra/inline-skills-scaffold Aug 7, 2026
7 checks passed
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.

1 participant