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, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check errors.Is(err, windows.ERROR_INVALID_PARAMETER) / os.ErrNotExist semantics — treat it as 'process gone' rather than retrying
  2. If ERROR_ACCESS_DENIED, run the caller with sufficient privileges (elevate, or run as the same user/service account as the target)
  3. Re-capture a fresh PID immediately before Capture to narrow the exit race
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/4ad5c491ee6a81d1. Report an issue: GitHub.