gastownhall/beads · error
procid: open process %d: %w
Error message
procid: open process %d: %w
What it means
Capture on Windows opens the target process with PROCESS_QUERY_LIMITED_INFORMATION via OpenProcess and wraps any failure with this message. It means the OS refused to open the PID — most commonly the process does not exist (stale PID), or an access-control issue, and the wrapped win32 error (e.g. ERROR_INVALID_PARAMETER 87, ERROR_ACCESS_DENIED 5) distinguishes which.
Source
Thrown at internal/procid/procid_windows.go:33
type Handle struct {
process windows.Handle
token Token
}
// errProcessExited marks a process which is terminated but whose PID is still
// resolvable because some handle (ours or a third party's, such as Task
// Manager or an antivirus scanner) keeps the process object alive. Treating
// it as gone keeps the invariant "Verify == true implies running" on Windows.
var errProcessExited = errors.New("procid: process has exited")
// stillActive is the GetExitCodeProcess sentinel for a running process
// (STILL_ACTIVE, 259).
const stillActive = 259
func Capture(pid int) (Token, error) {
process, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION)
if err != nil {
return "", fmt.Errorf("procid: open process %d: %w", pid, err)
}
defer func() { _ = windows.CloseHandle(process) }()
return tokenForProcess(process)
}
func Verify(pid int, tok Token) (bool, error) {
process, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION)
if err != nil {
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
return false, nil
}
return false, fmt.Errorf("procid: open process %d: %w", pid, err)
}
defer func() { _ = windows.CloseHandle(process) }()
current, err := tokenForProcess(process)
if err != nil {
if errors.Is(err, errProcessExited) {
return false, nilView on GitHub (pinned to 71377f2769)
Solutions
- Check errors.Is(err, windows.ERROR_INVALID_PARAMETER) / os.ErrNotExist semantics — treat it as 'process gone' rather than retrying
- If ERROR_ACCESS_DENIED, run the caller with sufficient privileges (elevate, or run as the same user/service account as the target)
- Re-capture a fresh PID immediately before Capture to narrow the exit race
- If PID reuse is a concern, capture the token as early as possible after spawning the child and verify with Verify(pid, tok) before signaling
Example fix
// before
pid := getStalePIDFromConfig()
tok, err := procid.Capture(pid)
if err != nil {
return err
}
// after
tok, err := procid.Capture(pid)
if err != nil {
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
return nil // stale PID: process already exited
}
return fmt.Errorf("capture pid %d: %w", pid, err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check before Capture on Windows
func canQuery(pid uint32) bool {
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return false
}
windows.CloseHandle(h)
return true
} Type guard
func isStalePID(err error) bool {
return errors.Is(err, windows.ERROR_INVALID_PARAMETER) || // process does not exist
errors.Is(err, os.ErrProcessDone)
}
func isAccessDenied(err error) bool {
return errors.Is(err, windows.ERROR_ACCESS_DENIED)
} Try / catch
tok, err := procid.Capture(pid)
switch {
case err == nil:
// proceed
case isStalePID(err):
return handleExitedProcess(pid)
case isAccessDenied(err):
return fmt.Errorf("need privileges to query pid %d (run elevated or same user): %w", pid, err)
default:
return fmt.Errorf("capture pid %d: %w", pid, err)
} Prevention
- Capture the token immediately after spawning the child to shrink the exit/PID-reuse race window
- Run the caller under an account with at least PROCESS_QUERY_LIMITED_INFORMATION access to targets (same user or elevated)
- Re-validate the PID right before Capture (windows.OpenProcess or tasklist) when working with externally supplied PIDs
- On long-lived hosts with high process churn, re-capture tokens rather than caching PIDs across restarts
When it happens
Trigger: Calling procid.Capture(pid) on Windows with a PID that has already exited (OpenProcess returns ERROR_INVALID_PARAMETER for nonexistent PIDs); targeting a process owned by another user/service without permission (ERROR_ACCESS_DENIED); targeting a system-protected process (e.g. anti-virus, protected-light processes) that denies even limited query; PID reuse where the PID now points to a different, inaccessible process.
Common situations: Health checks probing stale PIDs after a crash; capture racing process exit; running beads as a non-admin user while the target runs elevated or as LocalSystem; PID reuse after fast process churn on long-running Windows hosts.
Related errors
- procid: process has exited
- open process %d: %w
- lock already held by another process
- procid: malformed proc stat: missing comm terminator
- procid: malformed proc stat: missing starttime
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/4ad5c491ee6a81d1.
Report an issue: GitHub.