"Permission denied" renaming a folder on Windows is usually an open handle, not a permission

You try to rename a directory and get blocked:

# Git Bash
$ mv my-ai-team my-ai-team.old.1
mv: cannot move 'my-ai-team' to 'my-ai-team.old.1': Permission denied
# PowerShell, even elevated
PS> Move-Item .\my-ai-team .\my-ai-team.old.1
Move-Item: You do not have sufficient access rights to perform this operation
or the item is hidden, system, or read only.

The instinct is to reach for chown/takeown/icacls. On Windows that's usually the wrong tree to bark up. When even an administrator shell is denied, the cause is almost never the ACL — it's a runtime lock: some process has a file inside the directory open, or has the directory itself as its current working directory. Windows surfaces that lock as "Access denied," which reads like a permissions problem but isn't.

Rule out permissions in two commands

First confirm it really isn't attributes or ACLs, so you stop chasing them:

# Attributes: is it a reparse point / read-only / system / hidden?
Get-Item .\my-ai-team -Force | Format-List Name, Attributes, Target, LinkType

# ACLs: do you actually have rights?
icacls .\my-ai-team

In the case that prompted this note:

Attributes : Directory          # not ReparsePoint, not ReadOnly, not System
...
XEMT\David.Wei:(I)(OI)(CI)(F)   # inherited Full control — rights are fine

Clean attributes + your account with (F) + still denied = it's a lock, not a permission. Move on.

Note from Git Bash: icacls and attrib need real Windows paths, not /c/Users/.... Convert with cygpath:

icacls "$(cygpath -w ~/.local/share/my-ai-team)"

Also, handle.exe is a separate Sysinternals download — don't assume it's installed.

Find the process holding the directory

Two built-in ways, no install needed.

Resource MonitorresmonCPU tab → Associated Handles search box. Search a distinctive slice of the full path, not the bare folder name (you may have several folders by the same name):

share\my-ai-team

This lists every process with a handle under that path. In the real case it surfaced two bash.exe running ...\bin\mux-session-exit-hook — orphaned session-exit hooks from an agent's terminal multiplexer that never finished exiting.

Command line match — catches processes cwd'd into or launched against the path, which the handle search can miss (handles sometimes appear as \Device\HarddiskVolumeN\... instead of a drive letter):

Get-CimInstance Win32_Process |
  Where-Object { $_.CommandLine -like '*my-ai-team*' } |
  Select-Object ProcessId, Name, CommandLine | Format-List

Kill precisely, then rename

Target by the script/command that's stuck, not just by bash.exe, so you don't take down an unrelated Git Bash:

Get-CimInstance Win32_Process |
  Where-Object { $_.Name -eq 'bash.exe' -and $_.CommandLine -like '*mux-session-exit-hook*' } |
  ForEach-Object { Stop-Process -Id $_.ProcessId -Force }

Then the mv / Move-Item succeeds immediately. If you can't pin down the process, a reboot guarantees the handle is released.

Check state before settling on a name

If a previous rename attempt half-succeeded (folder renamed but old handles lingering), the handle path may already show the new name. Before deciding the final name, just look:

Get-ChildItem -Force | Where-Object Name -like 'my-ai-team*'

That tells you whether you still need .old.1 or whether it already exists and you want .old.2.

Key Points

  • On Windows, "Permission denied" / "access rights" on a rename — especially when an elevated shell also fails — usually means an open handle or a process cwd'd into the directory, not an ACL problem.
  • Rule out permissions fast: Get-Item -Force (attributes: ReparsePoint/ReadOnly/System) and icacls (your account has (F)?). Clean + still denied = it's a lock.
  • Find the locker with resmon → CPU → Associated Handles (search a distinctive path slice), or Get-CimInstance Win32_Process filtered on CommandLine.
  • Kill by the specific stuck command, not the bare interpreter name, to avoid collateral; a reboot is the guaranteed fallback.
  • From Git Bash, feed icacls/attrib real Windows paths via cygpath -w, and don't assume Sysinternals handle.exe is present.

Comments

  1. Markdown is allowed. HTML tags allowed: <strong>, <em>, <blockquote>, <code>, <pre>, <a>.