Repository navigation
Troubleshooting
Failure modes whose cause is visible in the code, with the fix.
The collector checks getuid() before anything else. Run it with sudo, or let
the LaunchDaemons run it.
ApiUrl is unset or unparseable. Check what the client actually resolved:
sudo defaults read /Library/Preferences/com.github.reportmate ApiUrl
Remember the precedence: a profile-forced value beats the system plist, and the
system plist beats an environment variable. Only --api-url on the command line
beats a profile. The run banner prints the effective ApiUrl under
=== FINAL CONFIGURATION ===.
To see whether a profile forces a value, read the managed preferences file.
sudo defaults read "/Library/Managed Preferences/com.github.reportmate"
Neither X-API-Key nor X-Client-Passphrase was accepted. Both headers are
sent when both are configured, and the API prefers the key and falls through to
the passphrase — so a bad ApiKey alone is enough to fail if no valid
passphrase is set. Set Passphrase (and ApiKey, if your deployment uses one)
and rerun.
The osquery managed_policies table reads TCC-protected plists under
/Library/Managed Preferences/. Without Full Disk Access, osqueryi hangs in a
TCC check with no UI to resolve, and the query eventually hits its timeout. The
macadmins extension has the same problem: it runs as its own process with its
own TCC identity, and without an independent grant it hangs in a sandbox check
while osqueryi blocks on its socket.
Deploy ReportMate-FullDiskAccess.mobileconfig with your Team ID substituted
for TEAMID0000. Grants for io.osquery.agent and macadmins_extension are
just as necessary as the one for the client itself.
Every step of the management module is isolated: a step that fails records an
entry in management._collection_errors and returns an empty default rather
than failing the module. A management payload that never arrives leaves the
server showing the previous record — which is how a re-enrolled Mac can report
its old provider indefinitely. Check _collection_errors in the cached
management.json before assuming the device really is on the old MDM.
Reboot detection compares the kernel's boot time, read from sysctl kern.boottime
and published at systemDetails.bootTime, with the previous run's value, with a
300-second tolerance for NTP clock corrections. A boot time derived as
now - uptime would slide forward by the length of each run and make every run
look like a reboot, so the client uses that value only as a fallback. If you see
this, confirm the system module is reporting systemDetails.bootTime; when it is
missing, the log says No kernel boot time reported, falling back to uptime-derived boot time.
The runner only launches an osquery binary that resolves to a root-owned file in
a directory chain nobody else can write, and that carries osquery's own
Developer ID signature. A Homebrew osquery under /opt/homebrew, a copy in a
user-writable folder, or a self-built binary is skipped, and with no trusted
candidate every module uses its shell fallback. Install the official osquery
pkg. See Architecture for the full search order.
A warning that names osquery extension at <path> failed ownership or signature checks means the same for the macadmins extension. In a signed build the
extension must also be signed with the runner's Team ID, which build.sh --sign
does to the bundled copy; an upstream copy placed by hand is refused.
Each osquery query races a timer — 30 s for built-in tables, 60 s for extension
tables — after which the child is SIGTERMed and then SIGKILLed. Extension
queries get the longer budget because the extension has a startup delay plus
registration time, and some of its tables do online I/O; sofa_unpatched_cves
fetches the SOFA feed. On a slow link, raise the budget:
sudo defaults write /Library/Preferences/com.github.reportmate ExtensionQueryTimeoutSeconds -int 120
To take the extension out of the picture entirely and rely on shell fallbacks:
sudo defaults write /Library/Preferences/com.github.reportmate ExtensionEnabled -bool false
macOS withholds the current SSID without Location Services. The client
substitutes the first known network and adds
"note": "SSID from known networks (location services disabled)" to
currentWiFiNetwork. Enable Location Services for the collector if you need the
real value.
The network module's throughput test measures actual bandwidth and takes 20–30
seconds every time that module runs. The hardware module's deep storage analysis
walks the root volume with du. The hourly daemon already passes
--storage-mode quick; if you are running the module by hand, pass it too.
Sources/Generated/ModuleVersions.swift is gitignored, so a fresh clone has no
ModuleVersions and a plain swift build fails. Sources/Core/AppVersion.swift
is committed as a placeholder, but an interrupted build can leave it truncated.
build.sh generates both and restores compilable placeholders when it exits, so
run it once without packaging.
./build.sh --skip-pkg --skip-zip --skip-dmg
The script requires Homebrew's bash 5.
brew install bash
launchctl disable is recorded in launchd's own override database, keyed by
label, independent of the plist file. Removing and reinstalling the plist does
not clear it and bootstrap keeps failing. The postinstall runs
launchctl enable system/<label> before bootstrapping for exactly this reason;
if you disabled a daemon manually, re-enable it the same way.
sudo launchctl enable system/com.github.reportmate.hourly
Watch for WARNING: failed to load <label> in the install log — a daemon that
silently fails to load is a module that silently stops being collected.
The postflight chain has three parts. Check all of them:
ls -l /usr/local/munki/postflight /usr/local/munki/postflight.d/
sudo tail -50 "/Library/Managed Reports/logs/reportmate-postflight.log"
reportmate.sh does not run the collection itself — it kickstarts
system/com.github.reportmate.installs. Munki allows a postflight only 60
seconds and launchd kills the whole process group when the run ends, so a child
started inline cannot survive. If the kickstart line logs an error, confirm the
installs daemon is loaded.
Scripts in postflight.d/ must be executable; the wrapper logs and skips
anything that is not.
The postinstall moves an existing postflight into postflight.d/ under a name
matching what it looked like (munkireport.sh, sal.sh, original.sh) and
runs it from there in name order. Confirm it is present and executable.
sudo chmod 755 /usr/local/munki/postflight.d/munkireport.sh
Each run prunes to the 24 newest timestamped directories. If you see hundreds, the client on that Mac does not include the prune; upgrade it.
Check these in order:
- The key is forced by a configuration profile. The GUI shows it locked, and the helper refuses to write or remove it.
- The key is not one the Prefs tab edits. The helper writes only those
thirteen keys; set the others with
defaultsor a profile. - The helper rejected the connection. It accepts only the app with identifier
com.github.reportmatesigned by the helper's own Team ID, so an unsigned or ad-hoc build, or an app signed with a different identity than the helper, is refused. - The helper daemon is not loaded. Without it the Run tab falls back to a direct, unprivileged run that fails the root check.
The helper logs each decision to the unified log.
log show --last 1h --predicate 'subsystem == "com.github.reportmate.helper"'
A dropped connection, timeout or HTTP 408, 429 or 5xx is retried up to
MaxRetryAttempts times (default 3) with 1 s and 2 s backoff, and each retry is
logged as Transmission attempt N/M failed. A run that still fails leaves its
payload in the cache, so it can be re-sent once the API is reachable.
sudo managedreportsrunner --transmit-only
MIT licensed. — reportmate.app