Winix.WhoHolds
0.4.0
Prefix Reserved
dotnet tool install --global Winix.WhoHolds --version 0.4.0
dotnet new tool-manifest
dotnet tool install --local Winix.WhoHolds --version 0.4.0
#tool dotnet:?package=Winix.WhoHolds&version=0.4.0
nuke :add-package Winix.WhoHolds --version 0.4.0
whoholds
Find which processes are holding a file lock or binding a network port.
lsof / handle replacement for diagnosing "file is locked" or "port is already in use" errors. Shows process name, PID, and owner for every holder. Works without admin rights for files; elevation improves results for ports.
Install
Scoop (Windows)
scoop bucket add winix https://github.com/Yortw/winix
scoop install winix/whoholds
Winget (Windows, stable releases)
winget install Winix.WhoHolds
.NET Tool (cross-platform)
dotnet tool install -g Winix.WhoHolds
Direct Download
Download native binaries from GitHub Releases.
Usage
whoholds [options] <target>
<target> is either a file path or a port specifier. whoholds auto-detects which you mean:
- File path — any argument containing a path separator, or an argument that names an existing file on disk.
:port— leading:is unambiguous:whoholds :8080always queries port 8080.- Bare number —
whoholds 8080queries port 8080 only if8080is not a file that exists on disk.
Examples
# Find what holds a DLL that can't be replaced
whoholds myapp.dll
# Find what is bound to port 8080 (TCP + UDP, IPv4 + IPv6)
whoholds :8080
# Bare number — treated as port if no file named "8080" exists
whoholds 8080
# Emit only PIDs (one per line) — suitable for piping
whoholds myapp.dll --pid-only
# Kill everything holding the file (pipe PIDs to wargs)
whoholds myapp.dll --pid-only | wargs taskkill /PID {} /F
# Machine-readable output for scripting
whoholds :8080 --json
# Show structured metadata about the tool itself
whoholds --describe
Options
| Option | Description |
|---|---|
--pid-only |
Output only PIDs (one per line). Suitable for piping to wargs or kill. |
--full-path, -l |
Show the full executable path instead of just the process name. Requires elevation for system processes. |
--json |
Output results as a JSON object on stdout (suite convention — pipe-friendly for jq). Envelope fields: tool, version, exit_code, exit_reason, plus processes array on success or error string on failure. Each process entry includes pid, name, path, state, and resource. |
--describe |
Output structured tool metadata as JSON (flags, examples, composability). |
--color[=auto\|always\|never] |
Coloured output: auto (default when omitted), always, or never. |
--no-color |
Disable coloured output. |
--help |
Show help and exit. |
--version |
Show version and exit. |
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success — query completed (even if no holders found). |
| 1 | Target not found or query error. |
| 125 | Usage error (bad arguments). |
Elevation Warning
whoholds always prints a warning to stderr when it is not running elevated (administrator on Windows, root on Linux/macOS). This is deliberate: without elevation, the Restart Manager API (Windows) and lsof (Linux/macOS) may only see processes belonging to the current user. A file held by a system service or another user's process will not appear in the results.
The warning reads:
Warning: Not elevated — only showing current user's processes.
This prevents the frustrating "it says nothing is holding the file, but I still can't delete it" scenario. If you see no holders but the problem persists, re-run elevated.
What changes when elevated
| Feature | Normal | Elevated |
|---|---|---|
| File lock detection | Current user's processes only | All processes |
| Port binding detection | Current user's processes only | All processes |
--full-path / -l |
May show empty paths for system processes (access denied) | Full executable paths for all processes |
| TCP state column | Always shown for port queries | Always shown for port queries |
For most developer workflows (your IDE, build tools, dev servers), normal mode is sufficient. Elevation is needed when a system service or another user's process holds the lock.
Platform Notes
| Platform | Implementation |
|---|---|
| Windows | File locks: Win32 Restart Manager API (no admin needed for current-user processes). Ports: IP Helper GetExtendedTcpTable / GetExtendedUdpTable (covers TCP + UDP, IPv4 + IPv6). |
| Linux / macOS | Delegates to lsof, which must be installed. On most distributions and macOS, lsof is present by default. |
Colour
- Process names are highlighted for quick scanning.
- Elevation warning is shown in yellow (stderr).
--no-colorsuppresses all ANSI colour output.- Respects the
NO_COLORenvironment variable (no-color.org).
Part of Winix
whoholds is part of the Winix CLI toolkit.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.0 | 142 | 6/13/2026 |
| 0.3.0 | 127 | 5/26/2026 |
| 0.3.0-rc2 | 125 | 5/10/2026 |
| 0.2.0 | 120 | 4/16/2026 |
| 0.2.0-test4 | 117 | 4/15/2026 |
## [0.3.0] - 2026-05-10
### Changed
- `--json` output now goes to **stdout** (was stderr) per suite convention. Pipe-friendly for `jq`; matches `man`, `winix`, `treex`, `files`.
### Fixed
- `lsof` rows whose command name contains spaces (multi-word names like `Google Chrome`) are no longer silently dropped. Pre-fix the fixed-column-index parser truncated at the first whitespace boundary, hiding a class of legitimate locks from the result list.
- Backend API failures (e.g. `lsof` exit non-zero, RM probe error) now route to **exit 1** with a stderr diagnostic. Pre-fix backend failures resulted in a silent empty result list with exit 0, indistinguishable from "no locks found".
- File-not-found path returns **exit 1** (was 125) per the documented contract — file-not-found is a target-state error, not a usage error.
- Stuck child stream reads (`lsof` hanging on a stale fd, RM probe wedged on a hung process) now surface a categorised stderr diagnostic with a timeout warning rather than silent hang.
- Windows `RM_UNIQUE_PROCESS` struct layout corrected — process names ≥ 64 chars no longer truncated mid-string.
- Restart Manager probe now retries on the eventual-consistency edge where a freshly-opened lock isn't yet visible to RM.
- `--version` output no longer carries the `+gitsha` SourceLink suffix.
### Added
- Library seam `Winix.WhoHolds.Cli.Run` for orchestration testing without process spawning.
### Documentation
- README and man page corrected: JSON example shape and field list now match the actual emitter.
- Elevation-warning text unified across surfaces.
- Wargs pipe example unified to `taskkill /PID {} /F` form for Windows users.
### Internal
- UTF-8 console adoption via `ConsoleEnv.UseUtf8Streams`.
- Standard `<PackageTags>` set on the NuGet package.
- ProcessRunner test seam added so backend integration tests can pin shell-output parsing without spawning real `lsof` / RM calls.
- Platform-skipped tests migrated to `SkippableFact + Skip.IfNot`.
See full changelog at https://github.com/Yortw/winix/blob/main/src/whoholds/CHANGELOG.md