gastownhall/beads · error

procid: process %d does not match token

Error message

procid: process %d does not match token

What it means

procid.Open successfully opened the process, but tokenForProcess returned a creation-time token different from the supplied token, so Open refuses to return a handle. This is the PID-reuse guard: the PID now refers to a different process instance than the one the token was minted for.

Source

Thrown at internal/procid/procid_windows.go:69

			return false, nil
		}
		return false, err
	}
	return current == tok, nil
}

func Open(pid int, tok Token) (*Handle, error) {
	process, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_TERMINATE)
	if err != nil {
		return nil, fmt.Errorf("procid: open process %d: %w", pid, err)
	}
	current, err := tokenForProcess(process)
	if err != nil || current != tok {
		_ = windows.CloseHandle(process)
		if err != nil {
			return nil, err
		}
		return nil, fmt.Errorf("procid: process %d does not match token", pid)
	}
	return &Handle{process: process, token: tok}, nil
}

func (h *Handle) Signal(os.Signal) error {
	if err := h.verify(); err != nil {
		if errors.Is(err, errProcessExited) {
			// The target exited on its own after Open; termination's goal is
			// already met.
			return nil
		}
		return err
	}
	if err := windows.TerminateProcess(h.process, 1); err != nil {
		if _, exitedErr := tokenForProcess(h.process); errors.Is(exitedErr, errProcessExited) {
			return nil
		}
		return fmt.Errorf("procid: terminate process: %w", err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat this as PID reuse: re-capture a fresh token with Capture(pid) before opening
  2. Verify with Verify(pid, tok) before Open to detect mismatch without error noise
  3. Persist the process creation time/token alongside the PID and refresh it whenever the target restarts
  4. Never share tokens across machines or OS platforms (token format is windows-v1 creation time)

Example fix

// before
h, err := procid.Open(pid, oldTok)
// after
if ok, _ := procid.Verify(pid, oldTok); !ok {
    newTok, terr := procid.Capture(pid)
    if terr != nil { return terr }
    oldTok = newTok
}
h, err := procid.Open(pid, oldTok)
Defensive patterns

Strategy: validation

Validate before calling

ok, err := procid.Verify(pid, tok)
if err != nil {
    return err
}
if !ok {
    tok, err = procid.Capture(pid)
    if err != nil {
        return err
    }
}

Type guard

func tokenMatches(pid int, tok procid.Token) bool {
    ok, err := procid.Verify(pid, tok)
    return err == nil && ok
}

Try / catch

h, err := procid.Open(pid, tok)
if err != nil {
    if strings.Contains(err.Error(), "does not match token") || tokenIsStale {
        newTok, cerr := procid.Capture(pid)
        if cerr != nil { return cerr }
        h, err = procid.Open(pid, newTok)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: Calling Open(pid, tok) after the original process exited and Windows recycled the PID for a new process; passing a token captured from a different PID.

Common situations: Long-running daemons holding tokens across restarts where the target was restarted and got the same PID; tokens persisted to disk and reused much later; copying a token between hosts or records.

Related errors


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