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
- Check errors.Is(err, unix.EPERM) and run the signaling code with sufficient privileges or as the same user as the target.
- If ESRCH, treat the process as gone (for non-fatal signals the library does not swallow it) and reconcile state.
- 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
- Run the signaling process as the same user as the target or with sufficient privileges
- Probe with signal 0 (syscall.Kill(pid, 0)) to check permission before real signals
- Account for sandbox/macOS hardened-runtime restrictions in dev vs prod
- Retry only on ESRCH-like transient errors, not EPERM
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
- procid: unsupported signal %v
- procid: process %d still matches token after fatal signal an
- proxy.ForceStopUnverified: signal pid %d: %w
- signal pid %d: %w
- dolt path is not executable
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/fe5b7409b6ce53dc.
Report an issue: GitHub.