gastownhall/beads · warning

procid: process %d no longer matches token

Error message

procid: process %d no longer matches token

What it means

This error comes from Handle.Signal on macOS. Before delivering a signal, the handle re-verifies that the target PID's process start time still matches the token captured earlier. If the process has exited and its PID was recycled by an unrelated process, the library refuses to signal, because the signal would hit a different process than the one the handle refers to.

Source

Thrown at internal/procid/procid_darwin.go:61

func Open(pid int, tok Token) (*Handle, error) {
	match, err := Verify(pid, tok)
	if err != nil {
		return nil, err
	}
	if !match {
		return nil, fmt.Errorf("procid: process %d does not match token", pid)
	}
	return &Handle{pid: pid, token: tok}, nil
}

func (h *Handle) Signal(sig os.Signal) error {
	match, err := Verify(h.pid, h.token)
	if err != nil {
		return err
	}
	if !match {
		return fmt.Errorf("procid: process %d no longer matches token", h.pid)
	}
	unixSig, ok := sig.(syscall.Signal)
	if !ok {
		return fmt.Errorf("procid: unsupported signal %v", sig)
	}
	if err := syscall.Kill(h.pid, unixSig); err != nil {
		if isFatalSignal(unixSig) && errors.Is(err, unix.ESRCH) {
			return nil
		}
		return fmt.Errorf("procid: signal %d: %w", h.pid, err)
	}
	if isFatalSignal(unixSig) {
		return h.confirmFatalSignal()
	}
	match, err = Verify(h.pid, h.token)
	if err != nil {
		return err
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat this as the target process being gone: check procid.IsProcessGone / re-Capture the PID and reconcile your records.
  2. Re-capture a fresh token with procid.Capture on the new process if you genuinely intend to control the replacement.
  3. Avoid holding procid Handles across process restarts; open a new Handle with procid.Open right before use.
  4. If this happens spuriously in tests, ensure the child process is still alive at signal time (check cmd.Process state).

Example fix

// before
handle, _ := procid.Open(pid, tok) // captured long ago
_ = handle.Kill() // PID may now belong to another process
// after
match, err := procid.Verify(pid, tok)
if err != nil || !match {
    // re-capture or re-spawn the target
    tok, err = procid.Capture(pid)
}
handle, _ := procid.Open(pid, tok)
_ = handle.Kill()
Defensive patterns

Strategy: try-catch

Validate before calling

match, err := procid.Verify(pid, tok)
if err != nil || !match {
    // process gone or replaced; do not signal
    return
}

Type guard

func stillValid(h *procid.Handle, pid int, tok procid.Token) bool {
    m, err := procid.Verify(pid, tok)
    return err == nil && m
}

Try / catch

if err := h.Kill(); err != nil {
    if strings.Contains(err.Error(), "no longer matches token") || procid.IsProcessGone(err) {
        // target exited or PID recycled: cleanup, re-capture, or re-spawn
        return
    }
    return fmt.Errorf("kill %d: %w", pid, err)
}

Prevention

When it happens

Trigger: Calling Handle.Signal (directly or via Handle.Kill) on a darwin Handle after the originally captured process has exited and another process now occupies the same PID number.

Common situations: Killing a stale recorded PID after the worker crashed and was replaced; long-lived supervisor reusing cached handles across restarts; PID reuse in constrained PID spaces on macOS.

Related errors


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