← Back to PR Bug Fix Database
7
High
PR-cookiecutter-2217

Fix: force_delete needs read+execute permissions, not just write

HighRepo: cookiecutter/cookiecutterDate: June 7, 2026
PR FixcookiecutterBug FixEdge CasePermissionsUnix

// The Bug

How cookiecutter/cookiecutter#2217 fixed PermissionError on read-only directories — why S_IWRITE alone is insufficient for shutil.rmtree on directories.

Repository
cookiecutter/cookiecutter
Issue
Status
PR-submitted

// Root Cause

Unix file permissions work differently for files vs directories:

| Permission | File Effect | Directory Effect |
|------------|-------------|------------------|
| `S_IREAD` (0o0400) | Read file contents | List directory entries (via `readdir`) |
| `S_IWRITE` (0o0200) | Write file contents | Create/delete entries in directory |
| `S_IEXEC` (0o0100) | Execute file as program | Access entries inside (via `stat`, `open`) |

`shutil.rmtree` needs to:
1. **Read** the directory to list its contents → needs `S_IREAD`
2. **Access** each entry to inspect it → needs `S_IEXEC`
3. **Delete** entries as it traverses → needs `S_IWRITE`

The original code only set `S_IWRITE`, so `rmtree` could list the directory (write permission on the parent) but couldn't access entries inside the read-only directory.

// The Fix

Diff showing the exact changes made to fix the bug.

@@ -28,7 +28,7 @@ def force_delete(func, path, _exc_info) -> None:  # type: ignore[no-untyped-def]
     Usage: `shutil.rmtree(path, onerror=force_delete)`
     From https://docs.python.org/3/library/shutil.html#rmtree-example
     """
-    os.chmod(path, stat.S_IWRITE)
+    os.chmod(path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
     func(path)


@@ -24,7 +24,7 @@ def test_force_delete(mocker, tmp_path) -> None:
     rmtree = mocker.Mock()
     utils.force_delete(rmtree, ro_file, sys.exc_info())

-    assert (ro_file.stat().st_mode & stat.S_IWRITE) == stat.S_IWRITE
+    assert (ro_file.stat().st_mode & stat.S_IRWXU) == stat.S_IRWXU
     rmtree.assert_called_once_with(ro_file)

     utils.rmtree(tmp_path)