[BUG] bmad setup fails on Windows when _bmad/ already exists (WinError 183 in replace_dir)

Author: blueshingCreated Sep 16, 2026Updated Sep 16, 2026

Description

On Windows, bmad setup fails with FileExistsError: [WinError 183] whenever {project-root}/_bmad already exists. The first setup run on a clean project succeeds (that path does a plain src.rename(dest)), but every subsequent run aborts.

The cause is in skills/bmad/scripts/setup.pyreplace_dir() (main, line 1178):

backup = Path(tempfile.mkdtemp(prefix="_bmad.old-", dir=dest.parent))
try:
    dest.rename(backup)

tempfile.mkdtemp() creates the backup directory, and then dest.rename(backup) is asked to rename onto it. On POSIX, rename() onto an existing empty directory succeeds, so this works on macOS/Linux. Windows MoveFileEx without MOVEFILE_REPLACE_EXISTING refuses any existing target — and it never replaces an existing directory — so the rename always raises.

replace_dir() is used by both setup and doctor (materialize_bmad / the doctor staging path), so bmad doctor is affected the same way on Windows.

Steps to reproduce

  1. On Windows, in a project directory, run bmad setup (via the bmad-toolbox plugin skill) — this succeeds and creates _bmad/.
  2. Run bmad setup again in the same project.
  3. The script exits non-zero with a traceback.

Equivalent direct invocation:

uv run --no-cache "{skill-root}/scripts/setup.py" --project-root "{project-root}" --skill "{skill-root}"

Expected behavior

The second run re-materializes _bmad/, preserving config.toml, custom/, and *.user.toml, and exits 0 — as it does on macOS/Linux.

Actual behavior

The run aborts. _bmad/ is left intact and the staging directory is cleaned up, so nothing is corrupted, but setup can never be re-run (and bmad doctor cannot repair) on Windows.

Relevant log output

Traceback (most recent call last):
  File "...\skills\bmad\scripts\setup.py", line 1628, in <module>
    raise SystemExit(main())
  File "...\skills\bmad\scripts\setup.py", line 174, in main
    setup(
  File "...\skills\bmad\scripts\setup.py", line 224, in setup
    materialize_bmad(
  File "...\skills\bmad\scripts\setup.py", line 1381, in materialize_bmad
    replace_dir(staging, bmad)
  File "...\skills\bmad\scripts\setup.py", line 1395, in replace_dir
    dest.rename(backup)
  File "...\Python313\Lib\pathlib\_local.py", line 767, in rename
    os.rename(self, target)
FileExistsError: [WinError 183] Cannot create a file when that file already exists:
  'D:\<project>\_bmad' -> 'D:\<project>\_bmad.old-_n_hj7x0'

(Line numbers above are from the 6.13.0-next plugin copy; the same function is at line 1174 on main.)

Suggested fix

Reserve the unique name, then release it before the rename:

backup = Path(tempfile.mkdtemp(prefix="_bmad.old-", dir=dest.parent))
backup.rmdir()
try:
    dest.rename(backup)

This keeps mkdtemp's uniqueness guarantee and leaves the existing rollback/cleanup logic untouched. I applied exactly this locally and bmad setup then re-ran cleanly and idempotently (3 consecutive runs, exit 0, config.toml and custom/ preserved, no _bmad.old-* left behind).

An alternative, if the tiny TOCTOU window matters, is generating a candidate name without creating it (e.g. dest.parent / f"_bmad.old-{uuid4().hex[:8]}" in a retry loop).

Environment

  • Module: Not sure / Other — shared bmad skill setup script (skills/bmad/scripts/setup.py)
  • BMad version: bmad-toolbox plugin 6.13.0-next
  • IDE: Claude Code
  • OS: Windows 11 (26200), Python 3.13 via uv 0.12.12

Source: bmad-code-org/BMAD-METHOD