gastownhall/beads · error

procid: signal %d: %w

Error message

procid: signal %d: %w

What it means

This wraps the raw errno from syscall.Kill when sending a signal fails for a reason other than the special fatal-signal/ESRCH case. The process still matched the token before the kill, but the kernel rejected the signal delivery — commonly due to permissions (EPERM) or the process disappearing in a non-fatal-signal race (ESRCH).

Source

Thrown at internal/procid/procid_darwin.go:71

}

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
	}
	if !match {
		return fmt.Errorf("procid: process %d no longer matches token", h.pid)
	}
	return nil
}

func (h *Handle) confirmFatalSignal() error {
	deadline := time.Now().Add(fallbackSignalConfirmTimeout)
	for {
		match, err := Verify(h.pid, h.token)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check errors.Is(err, unix.EPERM) and run the signaling code with sufficient privileges or as the same user as the target.
  2. If ESRCH, treat the process as gone (for non-fatal signals the library does not swallow it) and reconcile state.
  3. Verify the PID still matches your token and re-open the handle via procid.Open if your supervisor restarted the process.

Example fix

// before
err := handle.Signal(syscall.SIGUSR1) // procid: signal 1234: operation not permitted
// after
if err := handle.Signal(syscall.SIGUSR1); err != nil {
    if errors.Is(err, unix.EPERM) {
        // escalate, or log and fall back to killing via parent/supervisor
    } else if procid.IsProcessGone(err) {
        // target already exited
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure same-user target before signaling
if err := syscall.Kill(pid, 0); err != nil && errors.Is(err, unix.EPERM) {
    // will not be able to signal; escalate or skip
}

Try / catch

if err := h.Signal(syscall.SIGTERM); err != nil {
    var perr *os.SyscallError
    if errors.As(err, &perr) && errors.Is(perr.Err, unix.EPERM) {
        // permission denied: rerun with privileges or signal via parent
    } else if procid.IsProcessGone(err) {
        // target vanished; ignore
    }
    return err
}

Prevention

When it happens

Trigger: Handle.Signal on darwin where syscall.Kill returns an error: EPERM (no permission to signal the target, e.g. different uid, sandboxed), ESRCH while sending a non-fatal signal, EINVAL.

Common situations: Signaling a process owned by another user from a sandboxed/least-privileged app on macOS; container/CI runners restricting signals; target exiting between Verify and Kill when sending a non-fatal signal like SIGUSR1.

Related errors


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