auspex: IT Admin User Guide for Jamf Pro (macOS)
Last updated: August 20, 2026
This guide is for IT administrators deploying the Span agent (auspex) to macOS devices via Jamf Pro.
What This Package Does
auspex captures AI coding activity (prompts, file edits, tool use) from supported IDEs (Cursor, Claude Code, Codex, and more) and sends telemetry to Span's analytics backend. This data appears in your Span dashboard under AI Effectiveness.
The macOS installer is a single universal .pkg. When it installs elevated (as Jamf does), it places the auspex binary at /usr/local/bin/auspex, drops a root-owned managed configuration at /Library/Application Support/auspex/ (the managed.yaml marker = managed mode), and converges automatically (its postinstall runs auspex install --system, wiring machine-wide hooks and provisioning each engineer's identity, then starts the per-user agent at login).
You don't need a separate "install hooks" policy — the package converges itself. Your job is to (a) pre-authorize file access so users are never prompted, and (b) deliver each device's identity (token + work email) before the package installs.
Prerequisites
- A Span account with access to the integrations settings page.
- A Jamf Pro tenant managing the target macOS devices (macOS 13+, supervised), with each device's Email Address populated under User and Location in its inventory record (typically synced from your directory provider — LDAP, Cloud IdP, Jamf Connect, or SSO). Without it, the config script cannot determine the per-device work email.
- The macOS package URL, reachable by Jamf (or upload it as a package):
https://auspex.span.app/releases/v0.1.0/darwin/auspex_0.1.0_darwin_universal.pkg
Deployment order (important)
- Configuration profiles (file access + background items) — first.
- Email variable profile + identity script + package, bundled in one policy (script priority Before), so identity lands before the package converges.
Step 1: Enable the Integration and Get Your Token (one-time)
This step is the same regardless of your MDM.
Head to the AI tool settings dashboard (https://span.app/_/settings/integrations) and under IDE & CLI AI Tool Integrations, enable the tools your engineers use. Here is what each tool supports:
| Tool | What's captured |
|---|---|
| Claude Code | OTEL metrics + hooks |
| Cursor | Hooks |
| Codex CLI | OTEL metrics + hooks |
| Copilot CLI | Hooks |
| VSCode chat | Hooks |
Note: Some integrations don't yet have a dashboard toggle. If your engineers use one, enable Claude Code or Codex CLI to generate the token — it's shared across all tools. The token is org-wide.
Step 2: Deploy the File-Access + Background-Items Profile (before the package)
auspex reads the edited file from the project's git working tree the moment an AI edit fires. If a user keeps repos under a TCC-protected folder (Documents, Desktop, Downloads), that read would prompt:
"auspex" would like to access files in your Documents folder.
Deploy the combined profile below before the package so no one is prompted. It grants Full Disk Access (PPPC), pre-approves auspex's background items, and silences the background-activity notification.
Upload route (any Jamf version):
- Computers → Configuration Profiles → Upload.
- Select
auspex-background-items.mobileconfig. - Set Scope to the target group → Save.
The profile ships with Span's Developer ID team (KNH44SBYN4) and the binary path (/usr/local/bin/auspex) already filled in. To confirm the code requirement against a deployed Mac:
codesign -dr - /usr/local/bin/auspex
Tamper-proofing (recommended): deliver PayloadRemovalDisallowed on the managed profile so an enrolled user can't remove it. The package already ACL-protects /Library/Application Support/auspex on disk.
Requires a supervised device from a trusted MDM (macOS 13+).
Step 3: Deploy the Email Variable Profile (one-time)
Jamf policy scripts run locally on the device (as root) and can't read the server-side User and Location inventory directly. Deploy a Configuration Profile that uses Jamf's $EMAIL payload variable — Jamf substitutes the assigned user's email server-side and writes it to a managed plist on disk.
Save the following as span-email.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EMAIL</key>
<string>$EMAIL</string>
</dict>
</plist>
- In Jamf Pro: Computers → Configuration Profiles → New.
- General payload → Name:
auspex Email Variable. - Application & Custom Settings → Configure → External Applications → Add:
- Source: Upload File
- Preference Domain:
app.span.auspex - Upload PLIST File:
span-email.plist.
- Scope → the same group used in Step 2 → Save.
Jamf writes the resulting plist to /Library/Managed Preferences/app.span.auspex.plist. Verify on a test device:
/usr/libexec/PlistBuddy -c 'print :EMAIL' '/Library/Managed Preferences/app.span.auspex.plist'
If empty, populate the Email Address under Computers → select device → Inventory → User and Location.
Step 4: Add the Identity Script
In Jamf Pro: Settings → Computer Management → Scripts → New.
- General tab → Display Name:
auspex Identity. - Script tab → paste below, replacing
<YOUR_TOKEN>with the token from Step 1. - Save.
#!/bin/bash
set -e
# REPLACE with your org token from Step 1
SPAN_AUTH_TOKEN="<YOUR_TOKEN>"
if [ "$SPAN_AUTH_TOKEN" = "<YOUR_TOKEN>" ] || [ -z "$SPAN_AUTH_TOKEN" ]; then
echo "ERROR: SPAN_AUTH_TOKEN placeholder was not replaced in the script body."
exit 1
fi
PLIST="/Library/Managed Preferences/app.span.auspex.plist"
if [ ! -f "$PLIST" ]; then
echo "ERROR: Email variable profile not found at $PLIST. Complete Step 3 and let it install, then re-run."
exit 1
fi
WORK_EMAIL=$(/usr/libexec/PlistBuddy -c 'print :EMAIL' "$PLIST" 2>/dev/null || true)
if [ -z "$WORK_EMAIL" ] || [[ "$WORK_EMAIL" != *"@"* ]]; then
echo "ERROR: EMAIL in $PLIST is empty or invalid (got: '$WORK_EMAIL'). Populate User and Location > Email Address."
exit 1
fi
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
TOKEN_JSON=$(json_escape "$SPAN_AUTH_TOKEN")
EMAIL_JSON=$(json_escape "$WORK_EMAIL")
mkdir -p '/Library/Application Support/auspex'
printf '{\n "token": "%s",\n "work_email": "%s"\n}\n' "$TOKEN_JSON" "$EMAIL_JSON" \
> '/Library/Application Support/auspex/identity.json'
chown root:wheel '/Library/Application Support/auspex/identity.json'
chmod 644 '/Library/Application Support/auspex/identity.json'
echo "Success: identity.json written for $WORK_EMAIL"
The identity.json (token + work_email, root:wheel, 0644) is a delivery file the package's converge reads at install time, migrating it into each user's own private (0600) identity and then removing it.
Step 5: Upload the Package
In Jamf Pro: Settings → Computer Management → Packages → New → Display Name auspex → upload the universal .pkg (or configure your distribution point to fetch the URL above). Save.
Step 6: Create the Deployment Policy
This policy runs the identity script before the package, so identity.json exists when the package converges.
In Jamf Pro: Computers → Policies → New.
- General → Display Name
auspex Install; TriggerRecurring Check-in; Execution FrequencyOnce per computer. - Scripts → Configure → add auspex Identity with Priority = Before.
- Packages → Configure → add auspex (action Install).
- Scope → the same group as Steps 2–3.
- Save.
The package's postinstall converges the device to managed mode and starts the daemon.
Step 7: Smoke Test (before fleet rollout)
Validate on 1–2 pilot devices first.
-
Create a
auspex PilotSmart/Static group with your device and a teammate's. -
Temporarily scope the Step 2/3 profiles and the Step 6 policy to
auspex Pilot. -
On each pilot device:
sudo jamf policy. -
Confirm the email variable and identity delivery:
/usr/libexec/PlistBuddy -c 'print :EMAIL' '/Library/Managed Preferences/app.span.auspex.plist' -
Confirm the package installed and the device is managed:
pkgutil --pkg-info app.span.auspex auspex status # stderr: install mode: managed auspex auth show # work email + MASKED token -
In System Settings → Privacy & Security → Full Disk Access, auspex should show enabled and "managed by your organization."
-
Have the pilot user run Claude Code / Cursor and confirm traces in the dashboard.
-
Re-scope everything to production.
Verifying the Deployment
Computers → device → History → Policy Logs — the auspex Install policy should show Completed, with the identity script ending Success: identity.json written for <email>. On the device:
auspex status
auspex auth show
Optional: Replace legacy Span coding-hooks in the same rollout
If you're migrating from the older coding-hooks agent, you can retire it on each device as part of this rollout. This step is optional and best-effort: it removes coding-hooks only after auspex is confirmed installed, and only if the coding-hooks uninstaller is present — so it's safe to assign fleet-wide, including to devices that never had coding-hooks (there it simply no-ops).
Why bundle it: until coding-hooks is removed, a device running both agents will capture and send each event twice (duplicate traces). Gating removal on a confirmed auspex install lets you cut over cleanly, one device at a time, with no window where neither agent is running.
In Jamf Pro: add this to the same policy as the package (Step 6). Create it under Settings → Computer Management → Scripts (Display Name auspex Migrate coding-hooks), then in the policy's Scripts payload add it with Priority = After so it runs immediately after the package installs.
#!/bin/bash
# OPTIONAL post-install: retire legacy Span coding-hooks AFTER a confirmed auspex install.
# Safe anywhere — no-ops unless auspex is fully installed, and only removes coding-hooks
# if its uninstaller is present. Never fails the calling MDM step (always exits 0).
set -uo pipefail
CH_UNINSTALL="/Library/Application Support/app.span.coding-hooks/uninstall.sh"
# Gate 1 — auspex package receipt present (install confirmed by macOS).
if ! /usr/sbin/pkgutil --pkg-info app.span.auspex >/dev/null 2>&1; then
echo "auspex receipt not found — install not confirmed; leaving coding-hooks in place."
exit 0
fi
# Gate 2 — auspex binary AND managed marker both in place.
if [ ! -x "/usr/local/bin/auspex" ] || [ ! -f "/Library/Application Support/auspex/managed.yaml" ]; then
echo "auspex not fully in place (binary or managed marker missing) — leaving coding-hooks in place."
exit 0
fi
# auspex confirmed — best-effort remove coding-hooks, only if its uninstaller exists.
if [ -f "$CH_UNINSTALL" ]; then
echo "auspex confirmed — removing legacy coding-hooks…"
bash "$CH_UNINSTALL" || echo "coding-hooks uninstall returned non-zero (best-effort) — continuing."
echo "Done: coding-hooks removal attempted."
else
echo "No coding-hooks uninstaller found — nothing to remove."
fi
exit 0
Notes:
- The script exits 0 on every path, so a coding-hooks removal failure never fails the policy or disturbs auspex.
- It removes coding-hooks only when both auspex gates pass and the uninstaller exists at
/Library/Application Support/app.span.coding-hooks/uninstall.sh. On any other device it reports what it skipped and exits cleanly. - User-defined hooks in Cursor and Claude Code are preserved by the coding-hooks uninstaller.
Troubleshooting
Policy log shows ERROR: SPAN_AUTH_TOKEN placeholder was not replaced
Open Scripts → auspex Identity, replace <YOUR_TOKEN> with the real token, Save. Then Computers → device → History → Policy Logs, Flush auspex Install, and run sudo jamf policy.
EMAIL is empty or invalid
Populate User and Location → Email Address, Update Inventory, then Flush the policy and re-run sudo jamf policy.
Device is managed but capture isn't authenticated
The package converged before identity.json existed. Confirm the identity script ran (auspex auth show), then re-run sudo auspex install --system (the daemon hot-reloads identity). Ensure the script's Priority is Before in the policy.
User was prompted for Documents access
The file-access profile (Step 2) wasn't in place before the package. Deploy it, then on the device: sudo tccutil reset SystemPolicyAllFiles and re-check.
Upgrading
Upload the new package to the existing auspex package item, then Flush All on the auspex Install policy so Jamf re-delivers at next check-in. managed.yaml and edited fleet policy are preserved; the identity script only needs redeploying if the token changes.
Uninstalling
auspex ships two removers, one per tier. auspex uninstall removes a user-tier (self-installed) agent — its per-user service, binary, and hooks. auspex hooks uninstall --system un-wires the managed (machine-wide) capture hooks. Neither removes the machine-wide binary, launchd units, or root-owned managed config the package placed — a macOS .pkg has no built-in uninstaller and there is no auspex uninstall --system. So a clean managed removal runs both commands, then removes those machine-wide artifacts, then removes the MDM-delivered profiles.
Step 1 — Uninstall auspex
Run it elevated on the device, or deliver it through Jamf Pro (as a one-off policy, Scripts payload). It un-wires the hooks first (while the binary still exists), then removes the daemon units, the managed config, the binary, and the package receipt:
#!/bin/bash
set -uo pipefail
BIN="/usr/local/bin/auspex" # full path: root/MDM shells often omit /usr/local/bin from PATH
# 1a. Un-wire the MANAGED (machine-wide) capture hooks — the tier the MDM package wired.
"$BIN" hooks uninstall --system || true
# 1b. Clean up any lingering USER-tier install (e.g. a self-install from before the device
# was enrolled). `auspex uninstall` ONLY touches the per-user tier — it does not remove
# the managed tier, which is why 1a and the machine-wide removals below are still needed.
"$BIN" uninstall || true
# 2. Stop and remove the launchd units. Boot out the converge LaunchDaemon (system domain) AND
# the per-user daemon LaunchAgent for every logged-in user (gui/<uid>), then remove the plists.
launchctl bootout system "/Library/LaunchDaemons/app.span.auspex.converge.plist" 2>/dev/null || true
for u in /Users/*; do uid=$(id -u "$(basename "$u")" 2>/dev/null) || continue
launchctl bootout "gui/$uid" "/Library/LaunchAgents/app.span.auspex.plist" 2>/dev/null || true
done
rm -f "/Library/LaunchDaemons/app.span.auspex.converge.plist" "/Library/LaunchAgents/app.span.auspex.plist"
# 3. Remove the root-owned managed config tree (clear its tamper-proof ACL first).
chmod -N "/Library/Application Support/auspex" 2>/dev/null || true
rm -rf "/Library/Application Support/auspex"
# 4. Remove the binary and forget the package receipt.
rm -f "$BIN"
pkgutil --forget app.span.auspex 2>/dev/null || true
echo "auspex uninstalled"
On a self-installed (non-managed) Mac,
auspex uninstallon its own is the complete uninstaller — see the self-install guide. On a managed device it only clears a user-tier remnant, so the managed steps above are still required.
Step 2 — Remove the MDM-delivered profiles and assignment
In Jamf, unscope the auspex package/policy (Steps 5–6) and the configuration profiles (Step 2), and remove any PayloadRemovalDisallowed lock first. Do this so the MDM does not re-deliver auspex on the next check-in.
Verify the removal is complete
Run on the device — every line should print ok: (no receipt, binary, config, or daemon left):
pkgutil --pkg-info app.span.auspex >/dev/null 2>&1 && echo "STILL PRESENT: package receipt" || echo "ok: no package receipt"
command -v auspex >/dev/null 2>&1 && echo "STILL PRESENT: auspex on PATH ($(command -v auspex))" || echo "ok: auspex not on PATH"
[ -e /usr/local/bin/auspex ] && echo "STILL PRESENT: /usr/local/bin/auspex" || echo "ok: binary gone"
[ -e "/Library/Application Support/auspex" ] && echo "STILL PRESENT: managed config tree" || echo "ok: managed config gone"
launchctl print system/app.span.auspex.converge >/dev/null 2>&1 && echo "STILL LOADED: converge daemon" || echo "ok: converge daemon not loaded"
CU=$(stat -f%u /dev/console 2>/dev/null || echo 0); launchctl print "gui/$CU/app.span.auspex" >/dev/null 2>&1 && echo "STILL LOADED: per-user daemon (gui/$CU)" || echo "ok: per-user daemon not loaded"
ls /Library/LaunchDaemons/app.span.auspex.converge.plist /Library/LaunchAgents/app.span.auspex.plist >/dev/null 2>&1 && echo "STILL PRESENT: launchd units" || echo "ok: launchd units gone"
Fallback — only if the checks above still show remnants
A leftover usually means the tamper-proof ACL blocked removal, a launchd unit was still loaded, or an MDM profile re-pushed the managed config before Step 2 completed. First confirm Step 2 removed the MDM assignment (so nothing re-delivers), then force the removal:
#!/bin/bash
launchctl bootout system "/Library/LaunchDaemons/app.span.auspex.converge.plist" 2>/dev/null || true
# Boot out the per-user daemon agent for every user, best-effort.
for u in /Users/*; do uid=$(id -u "$(basename "$u")" 2>/dev/null) || continue
launchctl bootout "gui/$uid" "/Library/LaunchAgents/app.span.auspex.plist" 2>/dev/null || true
done
rm -f "/Library/LaunchDaemons/app.span.auspex.converge.plist" "/Library/LaunchAgents/app.span.auspex.plist"
chmod -R -N "/Library/Application Support/auspex" 2>/dev/null || true
rm -rf "/Library/Application Support/auspex"
rm -f /usr/local/bin/auspex
pkgutil --forget app.span.auspex 2>/dev/null || true
echo "auspex force-removed — re-run the verify checks above"
User-defined hooks in Cursor and Claude Code are preserved throughout.
Support
For issues not covered here, contact the Span team with:
auspex status
auspex support # redacted support bundle