pranshuparmar/witr · error

launchctl blame failed: %w

Error message

launchctl blame failed: %w

What it means

GetServiceLabel runs `launchctl blame <pid>` to discover which launchd service owns a process. Any non-zero exit or failure to capture output from that external command is wrapped in this error. Since launchctl blame only works on macOS and requires appropriate privileges, environment issues commonly surface here.

Source

Thrown at internal/launchd/plist.go:64

	Array   []string
	Dict    *plistDict
}

// plist search paths in order of precedence
var plistSearchPaths = []string{
	"~/Library/LaunchAgents",
	"/Library/LaunchAgents",
	"/Library/LaunchDaemons",
	"/System/Library/LaunchAgents",
	"/System/Library/LaunchDaemons",
}

// GetServiceLabel uses launchctl blame to get the service label for a PID
func GetServiceLabel(pid int) (string, string, error) {
	// launchctl blame <pid> returns the service that started the process
	out, err := exec.Command("launchctl", "blame", strconv.Itoa(pid)).Output()
	if err != nil {
		return "", "", fmt.Errorf("launchctl blame failed: %w", err)
	}

	// Output format varies:
	// - "system/com.apple.example" or "gui/501/com.example.app" (real service)
	// - "speculative", "non-ipc demand", "launch job demand", "ipc (mach)" (blame reasons)
	line := strings.TrimSpace(string(out))
	if line == "" {
		return "", "", fmt.Errorf("no service label found for pid %d", pid)
	}

	// Check if this is a real service path (contains "/" and starts with domain)
	if !strings.Contains(line, "/") {
		// This is a blame reason, not a service label
		// Try to find the service by querying launchctl list
		label, domain := findServiceByPID(pid)
		if label != "" {
			return label, domain, nil
		}

View on GitHub (pinned to dc4fa1da82)

Solutions

  1. Run on macOS and ensure launchctl exists (`which launchctl`).
  2. Retry with elevated privileges (sudo) if the target process belongs to another user.
  3. Confirm the pid is still alive before calling (signal 0 check).
  4. If blame is unsupported for that process, fall back to parsing the process environment or skip launchd enrichment.
  5. Inspect the wrapped %w error to distinguish exec-not-found from command failure.

Example fix

// before
label, domain, err := launchd.GetServiceLabel(pid) // fails on Linux
// after
if runtime.GOOS != "darwin" {
    return nil, fmt.Errorf("launchd info only available on macOS")
}
label, domain, err := launchd.GetServiceLabel(pid)
Defensive patterns

Strategy: fallback

Validate before calling

if runtime.GOOS != "darwin" {
    return errors.New("launchd enrichment requires macOS")
}
if _, err := exec.LookPath("launchctl"); err != nil {
    return errors.New("launchctl not available in PATH")
}

Try / catch

label, domain, err := launchd.GetServiceLabel(pid)
if err != nil {
    var ee *exec.ExitError
    if errors.As(err, &ee) && strings.Contains(err.Error(), "launchctl blame failed") {
        log.Printf("blame unavailable for pid %d (privileges or unsupported): %v", pid, err)
        return fallbackEnrichment(pid) // e.g. ps/env-based info
    }
    return err
}

Prevention

When it happens

Trigger: Calling GetLaunchdInfo/GetServiceLabel on macOS when launchctl blame exits non-zero: pid does not exist, insufficient privileges, launchctl not present, or non-macOS platform.

Common situations: Running on Linux/CI (no launchctl in PATH); inspecting a process owned by another user without sudo; the target pid exited between discovery and the blame call; sandboxed environments that block exec of launchctl.

Related errors


AI-assisted analysis of pranshuparmar/witr@dc4fa1da82 (2026-09-01). Data as JSON: /api/errors/7c025adf6522b815. Report an issue: GitHub.