Fix: force_delete needs read+execute permissions, not just write
How cookiecutter/cookiecutter#2217 fixed PermissionError on read-only directories — why S_IWRITE alone is insufficient for shutil.rmtree on directories.
The Bug
Repo: cookiecutter/cookiecutter Issue: #2217 Status: PR-submitted PR: https://github.com/cookiecutter/cookiecutter/pull/2231
When cookiecutter creates a project from a template stored in a read-only location (e.g., the Nix store with permissions d-w-------), cleanup via shutil.rmtree fails with PermissionError. The force_delete error handler only added S_IWRITE (owner-write, 0o200), which is insufficient for directories.
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:
- Read the directory to list its contents → needs
S_IREAD - Access each entry to inspect it → needs
S_IEXEC - 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
@@ -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)
The fix sets all three owner permission bits (S_IRWXU = 0o700), ensuring shutil.rmtree can fully traverse and delete the directory tree.
The Nix Store Connection
This bug was particularly impactful for Nix users. The Nix store (/nix/store) contains read-only copies of packages and their files. When a cookiecutter template is installed via Nix, copying from the store produces read-only directories. The original force_delete handler couldn’t clean these up, leaving users with PermissionError on project generation.
Python’s shutil.rmtree Documentation
Python’s shutil.rmtree docs explicitly provide this pattern:
import os, stat
import shutil
def remove_readonly(func, path, _):
os.chmod(path, stat.S_IWRITE)
func(path)
shutil.rmtree(directory, onerror=remove_readonly)
However, the Python docs example is for files, not directories. The cookiecutter fix correctly adapts this pattern for the directory case by adding S_IREAD | S_IEXEC.
This is a subtle but important distinction: the official Python documentation’s force_delete example works for read-only files but silently fails on read-only directories. Any code using this pattern should check whether it handles directories.
Transfer Pattern
When implementing error handlers for shutil.rmtree:
- Determine if the handler processes files, directories, or both
- For directories, always include
S_IREAD | stat.S_IEXECalongsideS_IWRITE - Test with both read-only files and read-only directories
Auto-generated from PR #2217. View all patches on GitHub.