"The file is open in another program." Okay — which one?
wholocks answers that in one command, then frees the file for you or waits until it's free. Windows, macOS, Linux. Zero dependencies — pure Python standard library.
中文说明 below.
$ wholocks report.docx
C:\work\report.docx
held by 1 process:
PID 4184 WINWORD.EXE Microsoft Word
exe: C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE
type: main window started: 2026-08-13 10:32:06 via: handle
tip: Close the document in Word. If the app is already closed, look
for a leftover '~$xxx' lock file next to the document.
free it: wholocks --kill report.docx
wait: wholocks --wait report.docx
Sooner or later you hit one of these:
- Windows: "The action can't be completed because the file is open in another program"
- Windows:
rmdir/rm -rf node_modulesfails with "Access is denied" or "The process cannot access the file" - Linux:
umount: /mnt/usb: target is busy - Any OS: a build fails because a stale dev server still holds
app.logordist/
The classic answers are painful: on Windows you download Sysinternals handle.exe or a 2010-era GUI unlocker and read raw handle dumps; on Linux/macOS you try to remember whether it was lsof -- file, fuser -v, or lsof +D dir, and none of it works on your teammate's Windows machine.
wholocks is one memorable command that works the same everywhere, tells you who holds the path, how (open handle, working directory, memory-mapped, executable), gives a targeted tip for well-known lockers (Office, OneDrive, antivirus, Explorer preview, orphaned dev servers...), and can kill the holder or block until the path is free.
pip install git+https://github.com/hc-ui/wholocksOr try it once without installing anything (needs uv):
uvx --from git+https://github.com/hc-ui/wholocks wholocks stubborn-folderPyPI release is being set up — pip install wholocks will work once it's live. Either way there are no dependencies; it's a single pure-stdlib package.
wholocks PATH [PATH...] # who holds this file/folder?
wholocks --kill PATH # terminate the holders (asks first)
wholocks --kill --yes PATH # ...without asking (scripts)
wholocks --wait -t 60 PATH # block until free, give up after 60 s
wholocks -r FOLDER # check everything inside a folder
wholocks --json PATH # machine-readable output
wholocks -q PATH # exit code onlyDelete a stubborn node_modules on Windows (orphaned esbuild.exe / node.exe workers are the usual culprits — Ctrl+C doesn't reach grandchild processes):
wholocks -r node_modules # see the orphans
wholocks -r --kill node_modules # free them, then delete normallyWait for Excel to release a report before your script copies it:
wholocks --wait --timeout 300 monthly.xlsx && cp monthly.xlsx archive/Find what blocks a USB drive from unmounting (Linux):
wholocks -r /mnt/usb"Why can't I delete the folder I'm standing in?"
wholocks . # spoiler: your own terminal is one of the holdersIn CI / scripts — exit codes make it composable:
| code | meaning |
|---|---|
| 0 | path is free (or was successfully freed / became free) |
| 1 | path is held (or kill failed / wait timed out) |
| 2 | usage error (bad flags, path does not exist) |
| 3 | platform backend unavailable |
wholocks -q app.db || echo "db is busy, skipping backup"from wholocks import find_holders
result = find_holders(["C:/work/report.docx"])
for h in result.holders:
print(h.pid, h.name, h.access, h.started)| Platform | Mechanism | Subprocesses spawned |
|---|---|---|
| Windows | Restart Manager API (the same API behind Windows' own "file is open in another program" dialog) plus a kernel-level NtQueryInformationFile handle query that also sees directory handles — Explorer windows and shells cd'd into a folder. Both via ctypes. |
none |
| Linux | Scans /proc/*/fd, maps, cwd, exe, root directly |
none |
| macOS | Drives the preinstalled lsof in its machine-readable -F mode |
lsof, ps |
No drivers, no elevated service, no kernel tricks. Killing uses TerminateProcess on Windows and SIGTERM (then SIGKILL with --force) on POSIX, always with a confirmation prompt unless --yes.
| wholocks | Sysinternals handle.exe |
Process Explorer / LockHunter | lsof / fuser |
lockhound (npm) |
|
|---|---|---|---|---|---|
| Windows | yes | yes | yes | no | yes |
| macOS / Linux | yes | no | no | yes | no |
| Install | one pip install |
manual download + EULA | manual download, GUI | preinstalled | needs Node |
| Readable answer | yes | raw handle dump | GUI browsing | terse columns | yes |
| Kill with guardrails | yes (confirm, refuses system/critical) | no | yes | fuser -k (no confirm) |
yes |
| Wait-until-free mode | yes | no | no | no | no |
Sees folder/cwd lockers (Explorer, cd'd shells) |
yes | needs admin | partial | fuser only |
heuristic |
| JSON + exit codes | yes | no | no | partial | no |
| Targeted tips (Office/OneDrive/AV...) | yes | no | no | no | partial |
| Dependencies | zero | – | – | – | zero (but Node) |
- Killing always shows what will die and asks first (
--yesto skip). - Refuses to touch critical system processes, PID 0–4 / PID 1, and itself.
- Windows services are refused unless
--force(stop them properly withnet stop). - Databases get a "stop the service instead" warning in the tip line.
--killverifies afterwards: success is reported only if the path is actually free.
- Windows, folder held via a deep subfolder: a shell
cd'd into the folder itself (or any probed subfolder) is detected — but in non-recursive mode only the folder and its immediate children are probed. If deletion still fails, run again with--recursive. - POSIX, other users' processes: without
sudo,/procandlsofonly reveal your own processes.wholockscounts what it couldn't inspect and says so instead of pretending the file is free. - Windows, multiple targets: the Restart Manager reports holders for the whole set of registered files, so per-file attribution isn't shown (Linux/macOS output lists the exact paths each process holds).
- PID reuse: between scan and kill a PID could in theory be recycled; the window is milliseconds, but it exists — the confirmation prompt shows names precisely for this reason.
- Network shares: holders on the other machine of an SMB/NFS share are invisible to local APIs; results cover local processes only.
Do I need admin/root? No for the common cases (your own processes). Root/admin widens visibility to other users' processes.
Why not use psutil? psutil is great, but it's a compiled dependency. wholocks is pure stdlib — it installs instantly anywhere Python runs, including locked-down corporate machines, and it uses the official Windows API for this exact question.
Does it close file handles without killing? No. Force-closing a handle inside another process (what some GUI unlockers do) can corrupt that process's state. wholocks prefers honest process termination with confirmation.
「操作无法完成,因为文件已在另一个程序中打开」——到底是哪个程序?
wholocks 一条命令告诉你是谁锁住了文件(或整个文件夹),还能帮你结束占用进程、或阻塞等待文件释放。Windows / macOS / Linux 全平台,零依赖,纯 Python 标准库。
- Windows:删除文件提示 「文件已在另一个程序中打开」、删除文件夹提示 「操作无法完成」
node_modules删不掉,提示被某个进程占用(Ctrl+C 杀不干净 esbuild/node 孤儿进程)- Linux:
umount: target is busy,U 盘弹不出来 - 脚本要处理的文件还被 Excel/WPS 开着,复制就失败
传统解法都很折磨:Windows 上要去下载 Sysinternals 的 handle.exe 或十几年前的 Unlocker 图形工具,对着一堆十六进制句柄猜;Linux/macOS 上要回忆 lsof 和 fuser 的参数;而且这些方法互不通用。
pip install git+https://github.com/hc-ui/wholocksPyPI 上架流程进行中,届时 pip install wholocks 即可。
wholocks 路径 # 谁在占用这个文件/文件夹?
wholocks --kill 路径 # 结束占用进程(先确认)
wholocks --kill --yes 路径 # 不确认,直接结束(适合脚本)
wholocks --wait -t 60 路径 # 阻塞等待释放,最多等 60 秒
wholocks -r 文件夹 # 检查文件夹内所有文件(递归)
wholocks --json 路径 # JSON 输出Windows 删除顽固的 node_modules:
wholocks -r node_modules # 看看是哪些孤儿进程
wholocks -r --kill node_modules # 清掉它们,然后正常删除等 Excel/WPS 关闭文件后再处理:
wholocks --wait --timeout 300 月报.xlsx && copy 月报.xlsx 存档\Linux 查 U 盘为什么弹不出:
wholocks -r /mnt/usb退出码:0 空闲/操作成功,1 被占用/失败/超时,2 参数错误,3 平台不支持——方便写脚本判断。
| 平台 | 机制 | 是否调用外部命令 |
|---|---|---|
| Windows | Restart Manager API(Windows 自家「文件已在另一个程序中打开」对话框背后的官方 API)加内核级 NtQueryInformationFile 句柄查询——后者连目录句柄都能查到:资源管理器开着文件夹窗口、某个终端 cd 在文件夹里,这类"隐形占用"也能揪出来 |
否 |
| Linux | 直接扫描 /proc/*/fd、maps、cwd、exe |
否 |
| macOS | 调用系统自带 lsof 的机器可读模式 |
lsof、ps |
对常见占用者(Word/Excel/WPS、OneDrive、杀毒软件、资源管理器预览、开发服务器孤儿进程、数据库等)会给出针对性提示,比如 Office 文档关了还占用时提醒你删掉旁边的 ~$ 锁文件。
- 结束进程前必先列出并确认(脚本用
--yes跳过) - 拒绝结束系统关键进程、PID 0–4 / PID 1、以及 wholocks 自己
- Windows 服务需要
--force才会动手(并提示用net stop更妥当) - 数据库进程会警告「请正常停止服务,强杀可能损坏数据」
--kill结束后会重新扫描验证,文件真正释放了才报成功
- Windows 深层子目录占用:终端
cd在目标文件夹本身或已探测的子目录里都能查到;非递归模式只探测文件夹本身和第一层内容,删不掉时加--recursive再查一次 - POSIX 非 root:只能看到自己的进程;查不到的进程数会如实报告,不会假装文件空闲
- 网络共享:SMB/NFS 上另一台机器的占用者本机 API 看不到,结果仅覆盖本机进程
- Windows 多目标:Restart Manager 返回的是整批文件的占用者,不区分单个文件(Linux/macOS 会列出每个进程具体占用的路径)
MIT