Skip to content
Sysadmin

What's Actually Holding This File? Two Approaches to Windows File Locks

“The action can’t be completed because the file is open in another program.” No program name. No PID. Just go away.

Resource Monitor can find it if you know where to look, but it’s slow and doesn’t help when the lock is held by a remote machine over SMB. I got tired of it and built two tools. One in Rust, one in pure C. Same problem, completely different design decisions.

The thing most people get wrong

SMB file locks and local process handles are not the same thing and don’t share an API.

If someone on your network has mounted a share and left a file open, that’s NetFileEnum from netapi32.dll — it enumerates open files across all active SMB sessions and gives you the user, path, and an ID you can pass to NetFileClose to kill it. Both my tools use this. Both require admin elevation.

The catch: NetFileEnum only sees SMB sessions. If Notepad on the same machine is holding the file, NetFileEnum returns nothing. For local handles you need something else — Process Explorer, Sysinternals Handle.exe, or NtQuerySystemInformation. I keep Process Explorer in my toolkit for exactly that reason.

LockSmith — Rust + egui

LockSmith is about 4 MB, starts in half a second, and runs in ~20 MB of RAM. Single eframe::App, calls NetFileEnum on refresh, renders a table with filter and force-close. I used egui instead of Tauri specifically for the cold-start time — when something is stuck and you need to kill a lock, 0.5 seconds beats 1.5 seconds.

One thing to get right with NetFileEnum: it doesn’t always return everything in one call. The resume_handle parameter is how you loop through the full result set. Forgetting to loop means you silently miss entries when there are a lot of open files.

HandleHunter — pure C, no runtime

Same job. 134 KB. No C runtime — no stdio, no malloc, nothing. Raw Win32 throughout.

The reason to do this isn’t cleverness — it’s that a recovery tool should have as few dependencies as possible. No DLL loader surprises, no “VCRUNTIME140.dll is missing” when you copy it to a server you’ve never touched before. It’s the binary I put on a USB stick.

For a tool any larger, this approach would be a maintenance nightmare. At 1,500 lines it’s manageable.

Neither of them sees local handles

For local process locks, the options are:

  • Sysinternals Handle.exe: handle64.exe -a -u C:\path\to\file — straightforward, elevated, does what it says
  • Process Explorer → Find → Find Handle or DLL: interactive version of the same
  • Building your own with NtQuerySystemInformation(SystemExtendedHandleInformation) — that’s how Sysinternals does it, but those structures are undocumented and have changed between Windows versions

I haven’t built the local handle case into either tool yet, mostly because the undocumented API makes it hard to commit to long-term. Might happen eventually.

Both tools on GitHub: LockSmith, HandleHunter. MIT licensed.

← All articles