// Enumerates running Claude processes and their --user-data-dir, without // spawning pgrep (avoids both the subprocess-per-check overhead and an // assumption that /usr/bin/pgrep exists at a fixed path). Uses the same // sysctl(KERN_PROC_ALL)/sysctl(KERN_PROCARGS2) technique ps/pgrep use // internally for same-user processes — no extra entitlement needed. // // Called on demand from live-state notification handlers (see AppDelegate), // never on a timer. import Darwin import Foundation struct RunningClaude { let pid: pid_t // nil means launched with no --user-data-dir flag at all (i.e. the // "default" profile). let userDataDir: String? } enum ProcessInspector { static func listRunningClaude(binaryPath: String) -> [RunningClaude] { allPIDs().compactMap { pid in guard let info = execInfo(pid: pid), info.execPath == binaryPath else { return nil } let flagPrefix = "--user-data-dir=" let dir = info.args.first(where: { $0.hasPrefix(flagPrefix) }).map { String($0.dropFirst(flagPrefix.count)) } return RunningClaude(pid: pid, userDataDir: dir) } } static func pid(forUserDataDir dir: String?, binaryPath: String) -> pid_t? { listRunningClaude(binaryPath: binaryPath).first { $0.userDataDir == dir }?.pid } private static func allPIDs() -> [pid_t] { var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0] var size = 0 guard sysctl(&mib, 4, nil, &size, nil, 0) == 0, size > 0 else { return [] } let capacity = size / MemoryLayout.stride + 1 var procs = [kinfo_proc](repeating: kinfo_proc(), count: capacity) var actualSize = capacity * MemoryLayout.stride guard sysctl(&mib, 4, &procs, &actualSize, nil, 0) == 0 else { return [] } let actualCount = actualSize / MemoryLayout.stride return procs[0.. ExecInfo? { var mib: [Int32] = [CTL_KERN, KERN_PROCARGS2, pid] var size = 0 guard sysctl(&mib, 3, nil, &size, nil, 0) == 0, size > MemoryLayout.size else { return nil } var buffer = [UInt8](repeating: 0, count: size) guard sysctl(&mib, 3, &buffer, &size, nil, 0) == 0 else { return nil } var argc = Int32(0) withUnsafeMutableBytes(of: &argc) { dst in dst.copyBytes(from: buffer[0...size]) } var offset = MemoryLayout.size func readCString() -> String? { guard offset < size else { return nil } let start = offset while offset < size, buffer[offset] != 0 { offset += 1 } guard offset > start else { return nil } let string = String(decoding: buffer[start..