Monocle inspects code and produces four audience-specific summaries: Executive, Security, Manager, Engineer

Background
Problem
Reviewing a 5k-line zsh script for leadership, a change board, the security team, and the engineer who has to maintain it is painful — and typically produces several different, inconsistent reports.
Inspiration
After attending Rob Potvin’s JNUC 2026 session — where he shared his quiz-my-code AI skill — on the plane ride home, I captured the following, heaven-inspired idea:
Create an AI skill to summarize code at various levels: Executive, Security, Manager, Engineer
But what to call it?
🔍 Monocle
Monocle inspects scripts and small repos (local or on GitHub), plus diagnostic and support bundles, and produces four audience-specific summaries (Executive, Security, Manager, Engineer) plus a Monocle Score.
Monocle is an Agent Skill tuned for shell (sh / bash / zsh), Python, AppleScript, Swift helpers, and Jamf Pro / macOS automation. While it pays particular attention to privilege elevation, silent failures, secrets, and environment assumptions, it aims to provide AI-assisted analysis, not a deterministic security scanner. Caveat emptor.
| View | For | Answers |
|---|---|---|
| 👔 Executive | Leadership, change boards | What it does, business risk, and a clear recommendation (up to 6 bullets) |
| 🔒 Security | Security reviewers | Severity-ranked findings, privilege map, secrets, network, and persistence |
| 📋 Manager | Team leads, service owners | Ownership, fragility, change risk, and prioritized action items |
| 🛠️ Engineer | Maintainers, reviewers | Control flow, footguns, edge cases, and before/after refactors with line references |
📈 Monocle Score
Every report starts at 100 and loses points for distinct issues:
| Issue | Deduction |
|---|---|
| 🔴 Critical / 🟠 High / 🟡 Medium / 🔵 Low Security finding | 40 / 20 / 8 / 3 |
| ⚪ Info Security finding | 0 |
| Non-security issue (Manager fragility, Engineer footgun / edge-case) | 2 each (max 20) |
The most severe finding also sets a hard band. The final score must land in (Critical 0–39, High 25–69, Medium 50–89, otherwise 70–100). A raw score outside that band is clamped. One serious finding cannot hide inside an otherwise clean report, and a pile of small issues cannot push a report into a worse band than its worst finding warrants.
Bands:
90–100 🟢 Excellent | 70–89 🔵 Good | 50–69 🟡 Fair | 25–49 🟠 Poor | 0–24 🔴 Critical
Philosophy: The score rates the code, not the Mac Admin. Documented powerful operations behind an admin-controlled gate (Jamf Pro parameters, policy scope, confirmation dialog, etc.) are not scored as defects. When severity depends on deployment choices, the headline score assumes the documented / safe baseline; the riskier alternate is shown separately, and the assumptions are listed as an Operator baseline checklist.
Installation
- Visit the Monocle releases page
- Download both the
.zipand the.zip.sha256from the latest release - In Terminal,
cd ~/Downloadsand validate the downloaded.zip- For example:
shasum -a 256 -c Monocle-initial-commit.zip.sha256
- For example:
- Only if you observe
OK, expand the.zip- For example,
unzip Monocle-initial-commit.zip
- For example,
- Validate the unpacked skill’s SHA256 matches the
skill_sha256hash displayed on the release page- For example,
python3 monocle/scripts/verify_report.py --manifest monocle
- For example,
- Only after you’ve confirmed a matching
skill_sha256, install the skill:- Claude Code
mkdir -pv ~/.claude/skills(as needed)rm -Rfv ~/.claude/skills/monocle(if upgrading, remove the old directory first)cp -Rv monocle ~/.claude/skills/
- Codex
mkdir -pv ~/.agents/skills(as needed)rm -Rfv ~/.agents/skills/monocle(if upgrading, remove the old directory first)cp -Rv monocle ~/.agents/skills/
- Claude Code
- Re-verify the installed skill
- Claude Code:
python3 ~/.claude/skills/monocle/scripts/verify_report.py --manifest ~/.claude/skills/monocle - Codex:
python3 ~/.agents/skills/monocle/scripts/verify_report.py --manifest ~/.agents/skills/monocle
- Claude Code:
Usage
Monocle activates automatically when a request matches its description; you can also call it by name.
Explicit invocation: Claude Code: /monocle; Codex: $monocle.
monocle this https://github.com/owner/repo/blob/main/script.zshmonocle review ./postinstallGive me the security view of this script(attach the file)
See Usage in the README.md for additional information.
Examples
Monocle reports may contain security-sensitive findings; by default they’re stored outside the target repository, and they should not be committed to source control without review.
DDM OS Reminder
Monocle Report: DDM-OS-Reminder
Monocle Score
Monocle Score: 79/100 (🔵 Good) — Approve
Score: 79/100 (🔵 Good) at the documented-deployment baseline — 3 Low, 1 Info, 6 non-security, or 76/100 (🔵 Good) if Force restart mode is enabled on Macs that lack Apple’s DDM state plist
Overall risk: 🔵 Low, or 🔵 Low if Force restart mode is enabled on Macs that lack Apple’s DDM state plist
Recommendation: Approve — no Critical, High, or Medium findings; the remaining gaps let a local user quiet their own reminders or affect admin-side reports, and none changes Apple’s DDM enforcement.
Generated by: Claude Opus 5.5 (claude-opus-5-5) · Skill: monocle
| Source | IDs | Count | Each | Deduction |
|---|---|---|---|---|
| 🔴 Critical | — | 0 | 40 | 0 |
| 🟠 High | — | 0 | 20 | 0 |
| 🟡 Medium | — | 0 | 8 | 0 |
| 🔵 Low | S1, S2, S3 | 3 | 3 | 9 |
| ⚪ Info | S4 | 1 | 0 | 0 |
| Non-security | fatal-exit relaunch loop; shared-log interaction history; dscl home parse; triplicated resolver; undocumented Apple log wording fails silent; swiftDialog update unverified | 6 | 2 (max 20) | 12 |
| Not scored | assemble RDNN regex and temp cleanup; createPlist eval; createSelfExtracting newest-file pick; preference-test dead trap; CSV helper echo escapes and parallel token handling; demo Force restarts the test Mac | 8 | 0 | 0 |
Total: 100 − 21 = 79 → 79/100 (🔵 Good)
Alternate: S4 becomes 🔵 Low when PastDeadlineRestartBehavior is Force on Macs without SoftwareUpdateDDMStatePersistence.plist: 100 − 24 = 76, no clamp → 76/100 (🔵 Good).
Operator baseline:
PastDeadlineRestartBehaviorstays at its defaultOff, orForceis used only on macOS versions where/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plistexists (S4 → Low if not).
Executive View
Executive View: Approve — well-hardened; residual gaps only quiet reminders
In one sentence: Shows Mac users a clear, branded reminder of the macOS update deadline that Apple’s device management already enforces, escalating as the deadline approaches and passes, so fewer Macs reach the forced deadline unprepared.
- Runs with full administrator access on every Mac it is deployed to, and keeps running in the background after restarts. It checks every minute whether a reminder is due; most checks finish immediately.
- Can restart Macs after the deadline, but only if administrators turn that on. The default is off; when set to “Force”, users get a countdown and may lose unsaved work.
- A determined user can silence their own reminders by writing fake entries into a system log every user can write to. This weakens the nudge, not the enforcement: Apple still installs the update at the deadline.
- The companion reporting tool for administrators can be fed spreadsheet formulas by anyone with admin rights on a managed Mac who renames it; the risk is to the administrator who opens the report, not to the fleet.
- Maintained by one person (about 96% of commits), with active releases and a documented security policy.
Monocle Score: 79/100 (🔵 Good), or 76/100 (🔵 Good) if Force restarts are enabled on older Macs that lack Apple’s update-state file
Recommendation: Approve — no Critical, High, or Medium findings; the remaining gaps let a local user quiet their own reminders or affect admin-side reports, and none changes Apple’s DDM enforcement.
Security View
Security View: 🔵 Low risk — 3 Low, 1 Info findings
Execution context: Deployment script runs as root via MDM policy; runtime dor.zsh and dor-starter.zsh run as root from a LaunchDaemon; UI and open calls drop to the console user with launchctl asuser … sudo -u. Resources/Jamf-getDDMstatusFromCSV.zsh runs as the administrator on their own workstation.
Overall risk: 🔵 Low — the code is defensively built; the remaining issues let a local user suppress their own reminders or inject formulas into an admin-side report, and none yields privilege escalation or alters Apple’s enforcement.
Findings
| ID | Severity | Title | Location |
|---|---|---|---|
| S1 | 🔵 Low | User-appendable install.log can suppress reminders despite corroboration |
reminderDialog.zsh:3750 |
| S2 | 🔵 Low | CSV formula injection in the Jamf Pro status report | Resources/Jamf-getDDMstatusFromCSV.zsh:898 |
| S3 | 🔵 Low | Jamf API helper leaves tokens live on error exits and accepts plain-HTTP server URLs | Resources/Jamf-getDDMstatusFromCSV.zsh:141 |
| S4 | ⚪ Info | Uncorroborated declarations trusted when Apple’s DDM state plist is absent (documented residual risk) | reminderDialog.zsh:3725 |
S1 — User-appendable install.log can suppress reminders despite corroboration · 🔵 Low
- Location:
reminderDialog.zsh:3750(invalid-declaration match),reminderDialog.zsh:3476(candidateHasNoMatchScanFailure),reminderDialog.zsh:3003(Update Tonight sender check),reminderDialog.zsh:3128(4,000-line window); the same resolver is copied intoResources/JamfEA-Pending_OS_Update_Date.zsh:712andResources/JamfEA-Pending_OS_Update_Version.zsh:673. - Origin: Platform + Code — any local user can append lines, sender tag included, to
/var/log/install.log(Known Apple platform behaviors: install.log is user-writable). The code corroborates positive declarations against the root-ownedSoftwareUpdateDDMStatePersistence.plist, but trusts negative and suppressive evidence from the log without corroboration. - Evidence:
if (( ${+ddmInvalidCandidateContexts[${candidateSignature}]} )); then(a “Failed to add declaration … Invalid declaration” line from any sender discards the real candidate);[[ ! "${logLine}" =~ ${ddmSoftwareUpdatedSenderRegex} ]](Update Tonight evidence accepted when the line merely carries asoftwareupdated[pid]:tag, whichlogger -t softwareupdated -iproduces). - Impact: A standard user can make the resolver return
missing,noMatch, or (by flooding past the 4,000-line window) find nothing, which suppresses all reminders, aggressive mode, and the Prompt/Force restart workflow; or forge Update Tonight evidence to pause normal reminders until midnight each day. The Pending OS Update EAs report the same wrong state to inventory. Apple’s DDM enforcement is unaffected. README’s claim that Update Tonight “Evidence must be logged bysoftwareupdated” (README.md:49) is not upheld. Traced from reading the code; not reproduced. - Fix: Treat the persistence plist as authoritative when it is available: if it lists an active declaration, keep reminding even when the log offers no candidate, an invalid marker, or a no-match marker, and log the disagreement at
[WARNING]. Require Update Tonight evidence to be corroborated by root-owned state (or drop the suppression), and state the residual log-spoofing risk for Update Tonight inSECURITY.md.
S2 — CSV formula injection in the Jamf Pro status report · 🔵 Low
- Location:
Resources/Jamf-getDDMstatusFromCSV.zsh:898(sanitizeForCsv), written atResources/Jamf-getDDMstatusFromCSV.zsh:3831, opened atResources/Jamf-getDDMstatusFromCSV.zsh:4215. - Origin: Code — device-reported inventory values are quoted for CSV structure but not neutralized for spreadsheet formulas.
- Evidence:
data="${data//\"/\"\"}"(only quotes and CR/LF are handled);open "${csvOutput}" >/dev/null 2>&1 &! - Impact: Anyone with local admin rights on one managed Mac can set its computer name to a value starting with
=,+,-, or@. When the administrator’s report opens in Excel or Numbers, the cell evaluates as a formula (for example aHYPERLINKlure or data-exfiltration formula). Requires the admin to open the report and, for most payloads, to click or enable content. - Fix: In
sanitizeForCsv, prefix a single quote when the value starts with=,+,-,@, tab, or CR:[[ "${data}" == [=+\-@$'\t'$'\r']* ]] && data="'${data}".
S3 — Jamf API helper leaves tokens live on error exits and accepts plain-HTTP server URLs · 🔵 Low
- Location:
Resources/Jamf-getDDMstatusFromCSV.zsh:141(cleanupEXIT trap),Resources/Jamf-getDDMstatusFromCSV.zsh:437andResources/Jamf-getDDMstatusFromCSV.zsh:463(typed URL),Resources/Jamf-getDDMstatusFromCSV.zsh:425(plist URL check). - Origin: Code — the normal exit invalidates the bearer token (
Resources/Jamf-getDDMstatusFromCSV.zsh:4190) and so does INT/TERM (Resources/Jamf-getDDMstatusFromCSV.zsh:205), butdie()paths such asResources/Jamf-getDDMstatusFromCSV.zsh:3137exit throughcleanup, which never callsinvalidateBearerToken. - Evidence:
die "Failed to process CSV file.";apiUrl="${userInput}"with nohttps://check (the plist path only checks!= http*). - Impact: A failed run leaves a valid Jamf Pro bearer token alive until it expires; a mistyped
http://URL sends basic-auth credentials or the client secret in cleartext. - Fix: Call
invalidateBearerTokenfromcleanup; reject anyapiUrlthat doesn’t match^https://at every entry point.
S4 — Uncorroborated declarations trusted when Apple’s DDM state plist is absent · ⚪ Info
- Location:
reminderDialog.zsh:3725,reminderDialog.zsh:3198 - Origin: Platform + Deployment — when
SoftwareUpdateDDMStatePersistence.plistis missing or unrecognized, the code falls back to trustinginstall.logtext, logs a[WARNING], andSECURITY.md:45documents this as accepted residual risk. - Evidence:
[[ "${ddmStatePersistenceStatus}" == "available" ]] || return 0 - Impact: At the documented default (
PastDeadlineRestartBehaviorOff), a forged past-due declaration only produces false reminders and wrong EA inventory, asSECURITY.mdstates. If an admin setsForce, a local user on such a Mac can forge a declaration two days past due and trigger the forced-restart countdown for whoever is at the console;SECURITY.mddoesn’t mention this. 🔵 Low in that configuration. - Fix: Never enter Force mode from an uncorroborated declaration (downgrade to Prompt), and extend
SECURITY.md:45to say so.
Low / Info roll-up:
- Credit:
PATHpinned to system directories and swiftDialog called by its root-owned bundle path (reminderDialog.zsh:20,launchDaemonManagement.zsh:41); per-runmktemp -ddialog directory with guarded cleanup (reminderDialog.zsh:4348); atomicmktemp+mv -freplacement of the runtime script, starter, fallback plist, and LaunchDaemon with ownership verified bystat(launchDaemonManagement.zsh:603,launchDaemonManagement.zsh:887);chown -hon owned assets; console-user language sanitized with an allowlist regex before it reaches${(P)}/printf -v(reminderDialog.zsh:571); swiftDialog pkg checked withspctl -a -vv -t installand Team IDPWA5…beforeinstaller(launchDaemonManagement.zsh:1126); allcurlcalls use--failand timeouts; CI actions SHA-pinned withpermissions: read-all; API credentials reach curl through--config -on stdin (Resources/Jamf-getDDMstatusFromCSV.zsh:655). - Info:
Resources/JamfEA-DDM-OS-Reminder-User-Clicks.zsh:17andResources/monitorRemoteSession.zsh:111append/usr/local:/usr/local/binto root’sPATH; system directories come first and every command used exists there, so no hijack path today, but it departs from the main scripts’ hardening. - Info: the Jamf CSV helper accepts the password as a positional argument and offers
--insecure; both are opt-in and print warnings (Resources/Jamf-getDDMstatusFromCSV.zsh:2918,Resources/Jamf-getDDMstatusFromCSV.zsh:637). - Info: agent configuration (
AGENTS.md,.github/copilot-instructions.md,.github/agents/,.github/instructions/) contains coding and style rules only, including a terse reply-mode preference; no reviewer-steering text and no session hooks.
Privilege map
- Install/replace swiftDialog pkg —
launchDaemonManagement.zsh:1131— root - Write
/Library/Management/<rdnn>/dor.zsh,dor-starter.zsh, fallback plist —launchDaemonManagement.zsh:603,launchDaemonManagement.zsh:331— root - Write and bootstrap LaunchDaemon
/Library/LaunchDaemons/<rdnn>.dor.plist; remove its quarantine xattr —launchDaemonManagement.zsh:887— root - Write
dor-state.plist,dor.pid—reminderDialog.zsh:777,reminderDialog.zsh:702— root - Download icons into a private
/var/tmpdirectory —reminderDialog.zsh:4405,reminderDialog.zsh:4427— root - Restart the Mac (
/sbin/shutdown -r now) in Force mode —reminderDialog.zsh:4682— root - Open System Settings / URLs, request restart via loginwindow —
reminderDialog.zsh:4693and thecase ${returncode}block — console user vialaunchctl asuser - Uninstall: removes only the project’s own files and empty directories —
launchDaemonManagement.zsh:510— root
Secrets inventory
- Jamf Pro API password or client secret — prompted with
read -s, or optional positional argument —Resources/Jamf-getDDMstatusFromCSV.zsh:539— passed to curl via stdin config; exposed inpsand shell history only when the admin uses the positional form. - Jamf Pro bearer token — in memory —
Resources/Jamf-getDDMstatusFromCSV.zsh:655— not logged; not invalidated ondie()paths (S3). - No secrets in the endpoint scripts or in MDM parameters (Parameters 4–6 carry a reset mode, a version, and a date).
Network egress
api.github.com/repos/swiftDialog/swiftDialog/releases/latestand thegithub.compkg URL — GET — TLS verified, URL prefix checked, Team ID verified —launchDaemonManagement.zsh:1095*.ics.services.jamfcloud.com/icon/…(or admin-set overlay URL) — GET — TLS verified by default,--fail --max-time 10—reminderDialog.zsh:4405,reminderDialog.zsh:4427- Admin-supplied Jamf Pro URL — GET/POST — TLS verified unless
--insecure; nohttpsenforcement on typed URLs (S3) —Resources/Jamf-getDDMstatusFromCSV.zsh:655
Persistence
- LaunchDaemon
/Library/LaunchDaemons/<rdnn>.dor.plist,StartInterval60,RunAtLoad, output to the project log —launchDaemonManagement.zsh:887 - Runtime scripts and state under
/Library/Management/<rdnn>/(root:wheel 0755) —launchDaemonManagement.zsh:510
Manager View
Manager View: Production-grade; single maintainer, active releases
Ownership: Dan K. Snelson — 517 of 536 GitHub contributions (about 96%), script headers point to snelson.us/ddm, SECURITY.md names a reporting address. Bus factor: 1.
Maturity: Production-grade — validates preferences and parameters, writes atomically, logs to a known path, versioned with a changelog, CI security gates, fails closed on unknown DDM states.
Change risk: High — root runtime on every in-scope Mac with a per-minute heartbeat; ~5,500-line runtime with resolver logic copied into two EAs; no automated behavior tests.
What it depends on
- Apple’s
install.logwording and the layout ofSoftwareUpdateDDMStatePersistence.plist— undocumented internals; a wording change silently turns reminders off (see hotspots). - swiftDialog 3.1.0.4994+ at
/Library/Application Support/Dialog/Dialog.app— installed from GitHub by the deployment script. - Managed preferences (
<rdnn>.dormconfiguration profile) — supply branding, schedule, and restart policy; RDNN must match across all generated files. - Jamf-hosted icon URLs — cosmetic; failures fall back to the Finder icon.
- One maintainer for releases and security fixes.
Fragility hotspots
- If no one is logged in, or swiftDialog is missing, the runtime exits fatally without advancing its schedule, so the starter relaunches it every minute; at the login window each run logs about 120 counter lines. You’d notice only from log volume —
reminderDialog.zsh:5058,reminderDialog.zsh:2700. - Quiet-period and periodic-reminder history is grepped from the shared organization log without filtering by script name, so other tools writing
Return Code:lines can suppress reminders; a 10 MB rotation wipes the history, and rotated.oldfiles are never pruned —reminderDialog.zsh:5269,reminderDialog.zsh:5014. - An Apple wording change in
install.logmakes the resolver returnmissing, which suppresses every reminder and is logged only as a notice; the Pending OS Update EAs then show no pending update —reminderDialog.zsh:4083. - The ~800-line DDM resolver exists three times (runtime plus two EAs); a fix applied to one copy drifts from the others —
reminderDialog.zsh:3681,Resources/JamfEA-Pending_OS_Update_Date.zsh:654,Resources/JamfEA-Pending_OS_Update_Version.zsh:615. - swiftDialog upgrades don’t check the
installerexit code or re-check the version, so a failed upgrade leaves the old version in place and deployment reports success —launchDaemonManagement.zsh:1131,launchDaemonManagement.zsh:1174.
Change & rollback
- Testing:
zsh -nsyntax checks, Semgrep, Gitleaks, and ShellCheck in CI;demomode andResources/reminderDialogPreferenceTest.zshfor manual checks. No automated behavior or resolver fixture tests in the repo. - Rollback: Clean. Parameter 4
Uninstallremoves the LaunchDaemon and every runtime file, leaving shared directories that aren’t empty; redeploying an older artifact replaces the runtime atomically. - Deploy path: MDM policy runs the assembled
Artifacts/ddm-os-reminder-<rdnn>-….zsh(or the self-extracting wrapper) as root; preferences arrive by configuration profile.
Action items
| # | Action | Owner role | Effort | Priority |
|---|---|---|---|---|
| 1 | Make the DDM state plist authoritative over log-based suppression; corroborate or drop Update Tonight suppression (S1) | Mac engineer | M | P2 |
| 2 | Block Force mode on uncorroborated declarations and update SECURITY.md (S4) |
Mac engineer | S | P2 |
| 3 | Neutralize formula prefixes in the CSV report; invalidate tokens on every exit; require https:// (S2, S3) |
Mac engineer | S | P2 |
| 4 | Advance or back off the schedule on fatal exits, and stop the per-second counter log at the login window | Mac engineer | S | P2 |
| 5 | Record interaction history in dor-state.plist rather than grepping the shared log; prune rotated logs |
Mac engineer | M | P3 |
| 6 | Move the DDM resolver into one shared source that assemble.zsh injects into the runtime and both EAs |
Mac engineer | M | P3 |
| 7 | Add a second maintainer or reviewer for releases | Service owner | M | P3 |
Engineer View
Engineer View: Careful resolver and file handling; fix fatal-exit scheduling and log-derived state
Shape: zsh; reminderDialog.zsh ~5,475 lines / 156 functions (runtime, deployed as dor.zsh), launchDaemonManagement.zsh ~1,289 lines / 43 functions (MDM entry point, embeds runtime and starter via heredoc), assemble.zsh builds artifacts; invoked as an MDM policy, then every 60 s by a LaunchDaemon running dor-starter.zsh.
Inputs → Outputs: MDM Parameters 4–6, managed/local <rdnn>.dorm plists, /var/log/install.log, SoftwareUpdateDDMStatePersistence.plist, dor-state.plist, console user’s .GlobalPreferences.plist → swiftDialog UI, dor-state.plist schedule, /var/log/<rdnn>.log, optional restart, exit codes.
Control flow
- Deployment: validate/install swiftDialog (
launchDaemonManagement.zsh:1156), reset per Parameter 4 (launchDaemonManagement.zsh:510), write or remove fallback plist from Parameters 5–6 (launchDaemonManagement.zsh:379). - Atomically install
dor.zshanddor-starter.zshafterzsh -n(launchDaemonManagement.zsh:603), then the LaunchDaemon, bootstrap, andkickstart -k(launchDaemonManagement.zsh:887). - Starter every 60 s: exit if
dor.pidis live orNextScheduledReminderis in the future orFALSE; otherwise launchdor.zshwithDOR_LAUNCH_SOURCE=starter(launchDaemonManagement.zsh:825). - Runtime globals: uptime, disk space via JXA with
diskutilfallback (reminderDialog.zsh:190); log pre-flight and 10 MB rotation (reminderDialog.zsh:4999); root check, PID file, traps (reminderDialog.zsh:5036). - Wait up to 120 s for a real console user, else
fatal(reminderDialog.zsh:5052). - Load preferences Managed → Local → Defaults with type validation (
reminderDialog.zsh:1960), validate cadence values (reminderDialog.zsh:2094), resolve language (reminderDialog.zsh:2584). demobranch (source copy only; stripped from assembled artifacts) simulates a deadline and displays (reminderDialog.zsh:5106).- Resolve DDM declaration from the last 4,000
install.loglines, corroborated against the persistence plist; onmissing/conflict/noMatch/invalidVersiontry the MDM fallback plist; unknown states fail closed (reminderDialog.zsh:4060). - Evaluate past-deadline Prompt/Force and aggressive mode, with kill-switch file (
reminderDialog.zsh:4542,reminderDialog.zsh:4582). - Suppression gates in order: display window and periodic reminder from log history, Update Tonight, quiet period, display-sleep assertions (up to
MeetingDelay) (reminderDialog.zsh:5251). - Build text, download icons, display swiftDialog; threshold monitor can refresh an open dialog (
reminderDialog.zsh:4708). - Handle return codes: Force mode loops until restart; otherwise schedule quiet-period or aggressive redisplay and open Software Update (
reminderDialog.zsh:4808). quitScriptwrites the next schedule and cleans the runtime directory and PID file (reminderDialog.zsh:4969);fatalexits through the EXIT trap only (reminderDialog.zsh:1538).
Footguns
- Fatal exits don’t schedule —
reminderDialog.zsh:423,reminderDialog.zsh:1538—fatalrunsexit 1, and the EXIT trap only cleans up, soNextScheduledReminderstays due and the starter relaunches every minute. Triggered by no console user for 120 s (reminderDialog.zsh:5058) or missing swiftDialog (reminderDialog.zsh:2700). Inferred from the code; not reproduced. - Interaction history from a shared log —
reminderDialog.zsh:5269—grep -n -E '\[INFO\].*Return Code: (0|2|3|4|10)'matches any tool writing to the organization log, not justdorm (. Log rotation atreminderDialog.zsh:5014drops all history, so the quiet period and the 28-day periodic window reset. dsclhome parse —reminderDialog.zsh:5069—awk -F ' ' '{print $2}'returns\n<last word>for a home path containing a space; language detection and dark-mode detection then read a nonexistent path and silently fall back to English and Light.- swiftDialog upgrade unchecked —
launchDaemonManagement.zsh:1131—installerstatus ignored;dialogCheckre-checks only-x(launchDaemonManagement.zsh:1174), so an older swiftDialog passes. - Demo Force restarts for real —
reminderDialog.zsh:4682—zsh reminderDialog.zsh demo ForcereachesexecuteRestartAction "Restart"and runs/sbin/shutdown -r nowon the admin’s test Mac when the timer expires. Not deployed (assembled artifacts strip demo mode).
Edge cases not handled
- Apple changes
install.logwording → resolver returnsmissing, reminders stop, logged only as NOTICE —reminderDialog.zsh:4083. - Persistence plist lists an active declaration but the log offers none in its last 4,000 lines →
missing, no reminder —reminderDialog.zsh:3784. - Login window with a due reminder → ~120 counter lines per minute in the log —
reminderDialog.zsh:5063.
Refactor suggestions
R1 — Back off the schedule on fatal exits · Quick win
reminderDialog.zsh:423
# before
function fatal() { updateScriptLog "[FATAL ERROR] ${1}"; exit 1; }
# after
function fatal() {
updateScriptLog "[FATAL ERROR] ${1}"
shouldManageDaemonScheduling && scheduleNextReminderInMinutes 15 "Retry after fatal error" >/dev/null 2>&1
exit 1
}
Stops the per-minute relaunch loop; scheduleNextReminderInMinutes is defined later but resolves at call time.
R2 — Let Apple’s persisted state veto log-based suppression · Deeper
reminderDialog.zsh:3750
# before
if (( ${+ddmInvalidCandidateContexts[${candidateSignature}]} )); then
# after
if (( ${+ddmInvalidCandidateContexts[${candidateSignature}]} )) \
&& ! { [[ "${ddmStatePersistenceStatus}" == "available" ]] && ddmDeclarationIsCorroborated "${parsedDDMVersionString}" "${parsedDDMEnforcedInstallDate}"; }; then
An invalid marker can no longer discard a declaration that softwareupdated still persists (S1). Apply the same rule to the noMatch and missing paths, and sync both EAs.
R3 — Neutralize spreadsheet formulas · Quick win
Resources/Jamf-getDDMstatusFromCSV.zsh:903
# before
data="${data//\"/\"\"}"
printf "%s" "${data}"
# after
data="${data//\"/\"\"}"
[[ "${data}" == [=+\-@$'\t']* ]] && data="'${data}"
printf "%s" "${data}"
Closes S2.
R4 — Robust home-directory lookup · Quick win
reminderDialog.zsh:5069
# before
loggedInUserHomeDirectory=$( dscl . read "/Users/${loggedInUser}" NFSHomeDirectory | awk -F ' ' '{print $2}' )
# after
loggedInUserHomeDirectory=$( dscacheutil -q user -a name "${loggedInUser}" | awk -F ': ' '/^dir:/ {print $2; exit}' )
Handles spaces in home paths.
R5 — Verify swiftDialog after install · Quick win
launchDaemonManagement.zsh:1131
# before
installer -pkg "$tempDirectory/Dialog.pkg" -target /
# after
if ! installer -pkg "$tempDirectory/Dialog.pkg" -target /; then
rm -Rf "$tempDirectory"
fatal "swiftDialog installer failed"
fi
Pair with an is-at-least re-check in dialogCheck after dialogInstall.
R6 — Single-source the resolver · Deeper
reminderDialog.zsh:3681
Move the resolver functions to one file under Resources/ and have assemble.zsh splice it into the runtime and both EAs, checked with cmp -s as it already does for the embedded message. Removes three-way drift.
Quick wins vs deeper work
- Quick wins (< 1 hr): R1, R3, R4, R5
- Deeper work: R2 — touches resolver semantics and both EAs, needs fixture tests; R6 — build-pipeline change.
Cross-cutting notes
Cross-cutting notes: log trust and log volume share one root — state kept in text logs
- Two kinds of log-derived state. S1 (Apple’s
install.log) and the interaction-history hotspot (the project’s own log) both decide behavior by grepping text. Moving project state into the root-owneddor-state.plistand preferring Apple’s persistence plist would close S1’s main path and the shared-log hotspot together. - Unattended schedule amplifies the fatal-exit loop. The per-minute heartbeat makes any fatal condition repeat around the clock; at the login window overnight it also inflates the shared log, which then rotates and erases the interaction history the quiet period depends on.
- Triplicated resolver amplifies S1. Fixing S1 in the runtime alone leaves the Pending OS Update EAs reporting forgeable inventory; R6 makes the fix land once.
Scope
Scope: 022a8da (main) — 27 files analyzed, about 64% direct read of the main runtime
| Field | Value |
|---|---|
| Date | 2026-10-04 19:22 EDT |
| Target | https://github.com/dan-snelson/DDM-OS-Reminder (GitHub repo) |
| Ref | 022a8da44b820ba3e74e0beef2095c89db137781 (main, v5.0.0) |
| Generated by | Claude Opus 5.5 (claude-opus-5-5) |
| Language(s) | zsh (all scripts #!/bin/zsh, most with --no-rcs); GitHub Actions YAML; XML plists |
| Files analyzed | 27 — reminderDialog.zsh, launchDaemonManagement.zsh, assemble.zsh, Resources/Jamf-getDDMstatusFromCSV.zsh, Resources/createSelfExtracting.zsh, Resources/createPlist.zsh, Resources/reminderDialogPreferenceTest.zsh, 7 Jamf EAs, Artifacts dev .zsh, and 12 more (see caveats) |
| Automated scan | semgrep 1.177.0 — p/r2c-security-audit, p/secrets, p/ci — 0 results (0 confirmed), 4 parse errors, 4 files skipped (3 size, 1 parser) |
| Scope caveats | See below |
Scope caveats:
- Oversized target. ~18,000 lines of zsh across the repo, over the small-repo limit; entry points and high-risk files read first.
- reminderDialog.zsh (5,475 lines) read directly: 1–430, 470–600, 686–900, 1354–1540, 1775–2045, 2094–2260, 2361–2430, 2584–2660, 2696–2815, 2964–3210, 3322–3560, 3681–3915, 4044–4100, 4244–4445, 4542–4625, 4677–5475 — 3,531 lines, about 64%. Pattern-scanned the rest, including date/locale formatting, scheduler threshold helpers (1000–1353),
candidateHasConflictingEvidence, andresolvePaddedEnforcementDateForCandidate. - launchDaemonManagement.zsh: read in full.
- Read by a delegated sub-review (read-only), then spot-checked:
Resources/Jamf-getDDMstatusFromCSV.zsh: 1–1039, 1110–1409, 1555–1664, 1939–2228, 2840–3259, 3420–3539, 3700–4220 read; remainder pattern-scanned.assemble.zsh: 1–110, 150–449, 564–720, 980–1059, 1290–1862 read; remainder pattern-scanned.Resources/createSelfExtracting.zsh: read in full.Resources/createPlist.zshandResources/reminderDialogPreferenceTest.zsh: header, cleanup, and sink regions read; remainder pattern-scanned.
- Extension Attributes: the five small EAs read in full;
JamfEA-Pending_OS_Update_Date.zshandJamfEA-Pending_OS_Update_Version.zshpattern-scanned (structure matches the runtime resolver). - Other files:
Resources/monitorRemoteSession.zsh,Resources/localizationFilter.zsh,Resources/checkUserFocusDisplayAssertions.zshscanned or read;.github/workflows/security-scan.ymlscanned for pins, permissions, and triggers; README, SECURITY.md, AGENTS.md, and agent config read. - Artifacts. The tracked dev artifact was diffed against both sources: identical except the runtime’s
tee -ais commented out (logging reaches the file through the LaunchDaemon’sStandardOutPath) and demo mode is stripped. The tracked plist and unsigned mobileconfig were covered by semgrep only. - Semgrep. The 4 parse errors are on
.github/workflows/security-scan.yml(embedded bash), a scanner limitation; 3 images skipped for size. Semgrep has no zsh parser, so 0 results doesn’t indicate clean code. - Not reproduced. S1, S4, and the fatal-exit relaunch loop were traced from the code; nothing in the target was executed. The
install.logforgery primitive is cited from Monocle’s Known Apple platform behaviors. - Untracked files. Fresh shallow clone; no local-only or gitignored files present.
Attestation:
monocle_attestation: v1
skill_repo: https://github.com/dan-snelson/Monocle
skill_commit: unknown
skill_ref: copied install
skill_tree: unavailable
skill_sha256: c2bb20f34568f6fe0103705ff21aa1151781dc886a9526819476d724ceaf51a3
skill_dirty: unknown
target: https://github.com/dan-snelson/DDM-OS-Reminder (GitHub repo)
target_ref: 022a8da44b820ba3e74e0beef2095c89db137781 (main, v5.0.0)
score_raw: 79
score_final: 79
score_alt: 76
band: Good
generated_by: Claude Opus 5.5 (claude-opus-5-5)
timestamp: 2026-10-04 19:22 EDT
Mac Health Check
Monocle Report: Mac-Health-Check
Monocle Score
Monocle Score: 80/100 (🔵 Good) — Approve with conditions
Score: 80/100 (🔵 Good) at the documented-deployment baseline — 4 Low, 1 Info, 4 non-security, or 72/100 (🔵 Good) if any Jamf Pro policy populates Parameter 5 or 8 with a reporting secret
Overall risk: 🔵 Low, or 🟡 Medium if any Jamf Pro policy populates Parameter 5 or 8 with a reporting secret
Recommendation: Approve with conditions — the code is low risk and carefully hardened; deliver the Splunk HEC token and webhook URL only through the root-only secrets file, with Parameters 5 and 8 blank in every policy.
Generated by: Claude Opus 5.5 (claude-opus-5-5) · Skill: monocle
| Source | IDs | Count | Each | Deduction |
|---|---|---|---|---|
| 🔴 Critical | — | 0 | 40 | 0 |
| 🟠 High | — | 0 | 20 | 0 |
| 🟡 Medium | — | 0 | 8 | 0 |
| 🔵 Low | S1, S2, S3, S4 | 4 | 3 | 12 |
| ⚪ Info | S5 | 1 | 0 | 0 |
| Non-security | Unvalidated Parameter 4; Network Quality false success; home-directory parse; early exits bypass the log | 4 | 2 (max 20) | 8 |
| Not scored | — | 0 | 0 | 0 |
Total: 100 − 20 = 80 → 80/100 (🔵 Good)
Alternate: with S5 at 🟡 Medium (a policy populates Parameter 5 or 8), 100 − 28 = 72, within the Medium range (50–89), no clamp → 72/100 (🔵 Good).
Operator baseline:
- The Splunk HEC token and webhook URL are delivered only in
/Library/Management/org.churchofjesuschrist/MacHealthCheck-Secrets.plist(root:wheel,600), and Parameters 5 and 8 are blank in every policy (S5 → 🟡 Medium if not). allowParameterSecretsstays"false"(S5 → 🟡 Medium if set to"true"and a parameter is populated).
Executive View
Executive View: Approve with conditions — low risk; keep reporting secrets out of policy settings
In one sentence: Gives employees a self-service health check of their Mac (updates, encryption, security agents, network, management status) and sends the same compliance results to the security team’s reporting system every night.
- Low risk overall. It reads settings and reports on them; it does not change security settings or remediate anything. The few local changes it makes (a nightly background job, an updated dialog tool, a log file) are documented.
- Runs with full administrator access on every Mac in scope, plus a nightly background job. The code protects that job well: it only installs a copy of itself from locations ordinary users cannot change.
- Reporting credentials are safe only if administrators store them as documented. If a Splunk token or chat-webhook address is typed into the policy’s script settings, any user on that Mac can read it while the check runs. The tool now refuses such values by default and warns about them.
- A user or a nearby Wi-Fi network can disrupt or skew reporting on one Mac. A Wi-Fi name containing a quotation mark stops every run on that Mac, and on some older setups a user can make their own Mac look more up to date than it is. Fleet-wide data and enforcement are not affected.
- One person maintains almost all of it (86% of commits), though the project is active, documented, and has automated security scanning.
Monocle Score: 80/100 (🔵 Good), or 72/100 (🔵 Good) if any policy still puts the Splunk token or chat webhook in its script settings
Recommendation: Approve with conditions — the code is low risk and carefully hardened; deliver the Splunk HEC token and webhook URL only through the root-only secrets file, with Parameters 5 and 8 blank in every policy.
Security View
Security View: 🔵 Low risk — 4 Low findings, 1 Info
Execution context: root via MDM policy (Jamf Pro Self Service or Silent policy, a self-extracting wrapper, or the pkg postinstall), and root via the nightly LaunchDaemon copy; user-facing work (swiftDialog, Homebrew, bioutil, Inspect summary) drops to the console user with launchctl asuser … sudo -u (Mac-Health-Check.zsh:2377).
Overall risk: 🔵 Low — no finding gives a local user root or exposes a secret at the documented baseline; 🟡 Medium if a policy populates Parameter 5 or 8.
Findings
| ID | Severity | Title | Location |
|---|---|---|---|
| S1 | 🔵 Low | Unescaped Wi-Fi name and identity values can abort every run or inject swiftDialog keys | Mac-Health-Check.zsh:1664 |
| S2 | 🔵 Low | Some compliance results fall back to user-writable evidence | Mac-Health-Check.zsh:7925 |
| S3 | 🔵 Low | swiftDialog install check parses spctl text and ignores its verdict |
Mac-Health-Check.zsh:7131 |
| S4 | 🔵 Low | Inspect summary assets expose the last user’s identity and results to every local account | Mac-Health-Check.zsh:6176 |
| S5 | ⚪ Info | Jamf policy parameters expose reporting secrets to local users (🟡 Medium if a policy populates Parameter 5 or 8) | Mac-Health-Check.zsh:163 |
S1 — Unescaped Wi-Fi name and identity values can abort every run or inject swiftDialog keys · 🔵 Low
- Location:
Mac-Health-Check.zsh:1628(helpmessage),Mac-Health-Check.zsh:1664,Mac-Health-Check.zsh:1676–1680 - Origin: Code —
helpmessageconcatenates${ssid},${computerName}and${loggedInUserFullname}raw into a JSON string literal; the script’s ownjsonEscape()andjq --arghelpers are not used here. - Evidence:
"helpmessage" : "'"${helpmessage}"'",followed byif ! validateJson "${mainDialogJSON}"; then … exit 1 - Impact: A Wi-Fi network name containing
"(any network the Mac joins, so attacker-chosen in a café or hotel) makes validation fail, and the script exits 1 before any check, report write, cached Splunk upload or nightly refresh, in every operation mode, includingSilent. A crafted SSID within the 32-byte limit can instead close the string and append keys such asinfobuttonaction;jqkeeps the last duplicate key, so the help button could open an attacker URL. Traced from code, not reproduced. A computer name or full name containing"causes the same abort. - Fix: Encode the value with
jqbefore building the template:helpmessageJSON=$( printf '%s' "${helpmessage}" | jq -Rs . ), then"helpmessage" : '"${helpmessageJSON}"',(see Engineer R1).
S2 — Some compliance results fall back to user-writable evidence · 🔵 Low
- Location:
Mac-Health-Check.zsh:7925(install.logDDM resolver),Mac-Health-Check.zsh:8069andMac-Health-Check.zsh:8096(per-user App Auto-Patch log),getEntraJamfAADPlistPath()(per-user JamfAAD plist) - Origin: Platform + Code — any local user can append lines, sender tag included, to
/var/log/install.log(Known Apple platform behaviors: install.log is user-writable); the code already prefers root-written sources (SoftwareUpdateDDMStatePersistence.plistwith an owner check atMac-Health-Check.zsh:7604–7606, the system App Auto-Patch logs) and falls back only when they are missing or unrecognized. - Evidence:
info "DDM Resolver: softwareupdated state plist unavailable or unrecognized; falling back to install.log" - Impact: On Macs without the state plist, or without a system App Auto-Patch log, a standard user can make their own Mac report “Up-to-date” for a pending DDM update or a recent App Auto-Patch run, and those values reach Splunk. DDM enforcement itself is unaffected; Apple installs at the deadline regardless. Limited to the user’s own device.
- Fix: Record the evidence source per check in the report (for example
evidenceSource: "user-writable") and downgrade fallback-sourced success towarning, so dashboards can filter it.
S3 — swiftDialog install check parses spctl text and ignores its verdict · 🔵 Low
- Location:
Mac-Health-Check.zsh:7131,Mac-Health-Check.zsh:7109,Mac-Health-Check.zsh:7136 - Origin: Code — the download is HTTPS-only and the Team ID is compared, but the comparison uses the
origin=text ofspctloutput whilespctl’s accept or reject result and exit status are discarded; the URL check accepts anyhttps://github.com/path. - Evidence:
teamID=$(spctl -a -vv -t install "$tempDirectory/Dialog.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()') - Impact: A package that
spctlrejects still installs as root whenever itsorigin=line ends in(PWA5E9TQ59). Exploiting this requires substituting the download (a GitHub API or release compromise, or TLS interception), so reachability is low. Inferred from the code;spctloutput for a forged certificate was not reproduced. - Fix: Require
spctl -a -t installto exit 0 and checkpkgutil --check-signatureforDeveloper ID Installer: … (PWA5E9TQ59); pin the URL prefix tohttps://github.com/swiftDialog/swiftDialog/releases/download/.
S4 — Inspect summary assets expose the last user’s identity and results to every local account · 🔵 Low
- Location:
Mac-Health-Check.zsh:6176,Mac-Health-Check.zsh:3853(writeReadableTextFile()),buildInspectUserInformationItemsJSON()andbuildInspectComputerInformationItemsJSON()(Mac-Health-Check.zsh:5726–5758) - Origin: Code — the config and compliance plist are deliberately
root:wheel 0644in/Library/Application Support/org.churchofjesuschrist/Inspectso swiftDialog can read them as the console user; the same release restricts the client log toroot:admin 640for exactly this data (Mac-Health-Check.zsh:6993–6994). - Evidence:
if ! chown root:wheel "${inspectFileToPrepare}" 2>/dev/null || ! chmod 644 "${inspectFileToPrepare}" 2>/dev/null; then - Impact: On shared Macs, any local account (or any process running as one) can read the last user’s full name, short name, UID, Platform SSO address, Wi-Fi name, IP address, VPN state and every health result. Silent and nightly runs refresh the file. Disclosure only; no integrity impact.
- Fix: Write the files
640 root:wheeland grant the console user read access with an ACL, as the script already does for the dialog files (Mac-Health-Check.zsh:1360–1363):chmod +a "${loggedInUser} allow read" "${inspectConfigPath}".
S5 — Jamf policy parameters expose reporting secrets to local users · ⚪ Info
- Alternate severity: 🟡 Medium while any policy populates Parameter 5 or 8 (SKILL.md Rule 11)
- Location:
Mac-Health-Check.zsh:108,Mac-Health-Check.zsh:163,enforceReportingSecretsSource()(Mac-Health-Check.zsh:320–340),Mac-Health-Check.zsh:473–478 - Origin: Platform + Deployment — Jamf Pro passes policy parameters as process arguments, and macOS lets any local user read them (observed on the analysis host, Darwin 25: a non-root
ps -axww -o user=,args=lists root processes’ full arguments). The code already does what it can: it rejects parameter secrets unlessallowParameterSecrets="true", loads them from a root-only600plist with owner, mode and symlink checks (Mac-Health-Check.zsh:290–297), exitsSilent+ production runs early when the only token was rejected, logs a warning, and hands secrets tocurlon stdin. - Evidence:
splunkHECToken="${8:-""}";allowParameterSecrets="false" - Impact: At the documented baseline there is no exposure. If a policy populates Parameter 5 or 8, the value stays readable in
argvfor the whole run (minutes for an interactive Self Service run) even when the script rejects it, and in the Jamf policy UI. The HEC token lets the holder inject forged events into the compliance index; the webhook URL lets the holder post to the channel. Both are narrow, write-only scopes, hence Medium. - Fix: Keep Parameters 5 and 8 blank, deliver both values in the secrets file, restrict the HEC token to the Mac Health Check index and sourcetype, and rotate any value that was ever set in a parameter.
Low / Info roll-up (credits): PATH excludes /usr/local/bin (Mac-Health-Check.zsh:35); isTrustedRootPath() checks every path component, sticky bit included (Mac-Health-Check.zsh:37–65), and gates the self-copy into the LaunchDaemon (Mac-Health-Check.zsh:4230); the sanitized copy must pass zsh -n and keep the Silent default (Mac-Health-Check.zsh:4278); jq is accepted only from root-owned paths; per-run mktemp -d with an EXIT trap (Mac-Health-Check.zsh:242–248); root-owned state with atomic mktemp + mv writes (writeSecureJSONFile()); cached reports, Inspect replay and the force-fresh trigger are trusted only when root-owned and not symlinks; Test and Development reports are isolated from the canonical report and HEC (Mac-Health-Check.zsh:4118), and the cache rejects non-production reports (Mac-Health-Check.zsh:396); an unrecognized Splunk mode fails safe to test (Mac-Health-Check.zsh:439–443); set -x starts after parameter parsing and secret-handling functions use noxtrace; HEC and webhook delivery use --proto '=https' --tlsv1.2, --max-time 15 and --fail-with-body; the Electron check validates version-shaped content so hard links cannot leak root-only files (Mac-Health-Check.zsh:10578); the self-extracting wrapper decodes into a root-only mktemp -d and forwards "$@" (Resources/createSelfExtracting.zsh:30–33); external checks set a fixed system PATH and call vendor tools by absolute path; CI uses SHA-pinned actions with permissions: read-all. Agent configuration (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, .github/agents/, .github/instructions/) contains coding guidance only, with no session hooks and no reviewer-steering text (Info).
Privilege map
- Health checks, discovery (
profiles,bputil,sysadminctl -secureTokenStatus,systemsetup,visudo -c,log show) — throughout — root ipconfig setverbose 1then0, restored only if it changed it —Mac-Health-Check.zsh:938–940— root- swiftDialog download and
installer -pkg—Mac-Health-Check.zsh:7136— root - Dock-named swiftDialog copy with ad hoc
codesign --force --sign -—prepareDockNamedDialogApp()— root killProcess "Dialog"(quits every swiftDialog window, documented) —Mac-Health-Check.zsh:7218— root- Self-copy to
/Library/Management/org.churchofjesuschrist/MHC.zshandlaunchctl bootstrap system—Mac-Health-Check.zsh:4375— root jamf policy -event <trigger>(external checks, each run as root by Jamf) andjamf recon—Mac-Health-Check.zsh:10157,updateComputerInventory()— rootchown "${loggedInUser}"of the per-user Inspect directory (inside a root-owned0755parent, so not swappable) —Mac-Health-Check.zsh:6216— root- swiftDialog, Inspect summary, Homebrew,
bioutil,security find-certificate—runAsUser(),runHomebrewAsUser()— console user (or the last user on nightly runs,Mac-Health-Check.zsh:969–978) CrowdStrike Falcon Status.bashwritesAppleLocaleto/Library/Preferences/.GlobalPreferences.plistand root’s preferences, restored by anEXITtrap —external-checks/CrowdStrike Falcon Status.bash:66–74— root
Secrets inventory
- Splunk HEC token — root-only secrets plist (documented) or Jamf Parameter 8 (rejected by default) —
Mac-Health-Check.zsh:163,Mac-Health-Check.zsh:301— sent in acurl --config -header on stdin (Mac-Health-Check.zsh:3926,Mac-Health-Check.zsh:3942); never logged. See S5. - Teams or Slack webhook URL (a bearer credential) — secrets plist or Jamf Parameter 5 (rejected by default) —
Mac-Health-Check.zsh:108,Mac-Health-Check.zsh:307— sent asurl = …on stdin (Mac-Health-Check.zsh:6528); never logged. See S5. - No hardcoded secrets. The Jamf Cloud icon URLs (
Mac-Health-Check.zsh:571) are public asset links.
Network egress
- Splunk HEC (Parameter 7) — POST —
https://enforced, TLS 1.2+, token on stdin —Mac-Health-Check.zsh:3919–3950 - Teams or Slack webhook — POST —
https://enforced, TLS 1.2+ —Mac-Health-Check.zsh:6427,sendWebhookPayload() api.github.com(swiftDialog and Homebrew release metadata) andgithub.com(swiftDialog pkg) — GET — default TLS —getLatestSwiftDialogPkgURL(),Mac-Health-Check.zsh:10394sofafeed.macadmins.io(macOS release feed) — GET with ETag,--fail --max-time 10—Mac-Health-Check.zsh:7311- Dock icon and overlay icon URLs (organization-configured) — GET as root into the per-run directory —
resolveDockIcon(),Mac-Health-Check.zsh:1406 - Apple service and Jamf hosts (
ncorcurl), ApplenetworkQuality, clock-skew comparison —checkNetworkHosts(),Mac-Health-Check.zsh:10317,checkClockSkew()(pattern-scanned) - Jamf Pro (policy triggers and inventory) — through the
jamfbinary
Persistence
- LaunchDaemon
org.churchofjesuschrist.MHC—/Library/LaunchDaemons/org.churchofjesuschrist.MHC.plistruns/Library/Management/org.churchofjesuschrist/MHC.zshas root daily at 00:53 with ±30 minutes of per-device jitter; stdout and stderr go to/dev/null—installClientSideScript()(Mac-Health-Check.zsh:4291–4328) - swiftDialog app (installed or updated outside
Silent) —/Library/Application Support/Dialog/Dialog.app—dialogInstall() - Root-only state: canonical report, SOFA and
networkQualitycaches, 14-day memory-pressure history —/Library/Management/org.churchofjesuschrist/ - Inspect assets —
/Library/Application Support/org.churchofjesuschrist/Inspect/(see S4)
Manager View
Manager View: Production-grade; one primary maintainer (86% of commits)
Ownership: Dan K. Snelson (@dan-snelson) — script header and SECURITY.md; 273 of 317 commits on GitHub, next contributor 16. Bus factor: effectively 1, with an active contributor community.
Maturity: Production-grade — versioned, extensively documented (README, SECURITY.md, Diagrams/, changelog), five operation modes, structured JSON reporting, CI security scanning, and a 5.0.0 release focused on hardening; a few input edge cases remain.
Change risk: Medium — root on every scoped Mac plus a nightly root job, in an 11,491-line single file with no automated functional tests; mitigated by zsh -n gates, mode isolation and fail-safe defaults.
What it depends on
- swiftDialog 3.1.1.4997+ — auto-installed from GitHub outside
Silent; GitHub downloads must be reachable. jq—/usr/bin/jqon macOS 15+, otherwise a root-owned install; a user-owned Homebrewjqis rejected.- Jamf Pro policies with custom triggers for external checks (
symvBeyondTrustPMfM,symvCiscoUmbrella,symvCrowdStrikeFalcon,symvGlobalProtect) — enabled by default on Jamf Pro; a missing policy reportsErroron every run. - Splunk HEC endpoint, index and sourcetype, plus the root-only secrets plist — required for compliance reporting.
- SOFA feed (
sofafeed.macadmins.io) — decides OS compliance. - Undocumented Apple internals:
SoftwareUpdateDDMStatePersistence.plistlayout andinstall.logwording; Apple can change either in any release. - Organization values hardcoded in source:
org.churchofjesuschristdomain, support contacts, branding URLs, VPN vendor.
Fragility hotspots
- A Wi-Fi name, computer name or full name containing
"stops every run on that Mac, including nightly refreshes; the error prints to stdout only, so you would notice only a stale Splunk record (S1) —Mac-Health-Check.zsh:1676. - A misspelled operation mode in Parameter 4 (
silent,Developer) runs the full interactive suite, writes the canonical report and installs the nightly job; the README warns about it, but the code does not reject it —Mac-Health-Check.zsh:105. - On Macs without
/usr/bin/jq(macOS 14 and earlier) and no root-ownedjq, the script exits with a misleading “mainDialogJSON is invalid JSON” before its own clearjqerror, and nothing reaches the log —Mac-Health-Check.zsh:1676vsMac-Health-Check.zsh:7005. - The nightly job discards stdout and stderr, so early exits during the 00:53 run leave no trace —
Mac-Health-Check.zsh:4322–4325. - A failed Network Quality test is reported as healthy with a blank speed and cached for 4 hours —
Mac-Health-Check.zsh:10346. - DDM detection depends on undocumented Apple file formats; a layout change silently moves results to the
install.logfallback (S2).
Change & rollback
- Testing: CI runs Semgrep, Gitleaks,
zsh -nand ShellCheck (.github/workflows/security-scan.yml); the deployment instructions require a five-mode regression on a test Mac. No automated functional tests. - Rollback: Clean. Remove the LaunchDaemon (
launchctl bootout system/org.churchofjesuschrist.MHC, delete the plist),/Library/Management/org.churchofjesuschrist/and/Library/Application Support/org.churchofjesuschrist/; swiftDialog can stay. No security settings are changed. - Deploy path: Jamf Pro script policy (Self Service or Silent), self-extracting wrapper (
Resources/createSelfExtracting.zsh), or a pkg fromResources/Makefilewhosepostinstallruns Self Service mode at install time; Fleet ships osquery policies only (Resources/mac-health-check-fleet-policies.yml).
Action items
| # | Action | Owner role | Effort | Priority |
|---|---|---|---|---|
| 1 | Confirm every Mac Health Check policy leaves Parameters 5 and 8 blank and uses the secrets plist; rotate any token that was ever in a parameter (S5) | Jamf admin + Security | S | P1 |
| 2 | JSON-encode helpmessage so Wi-Fi, computer and user names cannot break or inject into the dialog (S1) |
Mac engineer | S | P2 |
| 3 | Reject unrecognized Parameter 4 values and move the jq and root pre-flight ahead of the first JSON use, logging early exits |
Mac engineer | S | P2 |
| 4 | Restrict Inspect assets to the console user with an ACL (S4) | Mac engineer | S | P2 |
| 5 | Require spctl acceptance plus pkgutil Team ID verification for swiftDialog installs; pin the release URL (S3) |
Mac engineer | S | P2 |
| 6 | Tag fallback-sourced results (install.log, per-user logs) in the report and filter them in the Splunk dashboard (S2) |
Mac engineer + Splunk owner | M | P3 |
| 7 | Report Network Quality failures as error and fix the home-directory parse |
Mac engineer | S | P3 |
| 8 | Name a second maintainer with release rights | Service owner | M | P3 |
Engineer View
Engineer View: Strong trust boundaries; fix unescaped dialog JSON and early-exit ordering
Shape: zsh (#!/bin/zsh --no-rcs), 11,491 lines, about 220 functions, single top-level script; invoked by Jamf Pro ($4–$11), a self-extracting wrapper, a pkg postinstall (Resources/postInstall.zsh:6), or the LaunchDaemon (no arguments, launchDaemonRun=true).
Inputs → Outputs: Parameters 4–11, launchDaemonRun env, secrets plist, MDM profiles, system and user preference files, install.log, external Jamf policy output, network responses → swiftDialog UI, /var/log/org.churchofjesuschrist.log, canonical JSON report, Splunk HEC event, Teams/Slack message, Inspect assets, LaunchDaemon, exit code (0 healthy or warning, 1 failure; in Silent + production, delivery success).
Control flow
- Harden
PATH, resolve a root-ownedjq, parse Parameters 4–11, compute secret-free flags, then enableset -xfor Debug —Mac-Health-Check.zsh:35–184. - Create the per-run
mktemp -ddirectory with anEXITtrap; load the secrets plist and reject parameter secrets —Mac-Health-Check.zsh:242–343. - Normalize the Splunk mode (unknown →
test);Silent+ production with only a rejected Parameter 8 exits 1 —Mac-Health-Check.zsh:429–478. - Cache gate (
Silent+ production): a root-owned force-fresh trigger or Parameter 11 forces a full run; otherwise a matching clientscriptVersionplus a valid root-owned cached report setsclientSideSkipChecks—Mac-Health-Check.zsh:481–553. - Discovery: MDM vendor from
profiles(Jamf requiresjamf.log, else exit 1), hardware, user, SSID, boot policies, VPN —Mac-Health-Check.zsh:768–1314. - Create the dialog command and JSON files (
600plus a read ACL), buildmainDialogJSONand the per-MDM list items, exit 1 if any fails validation —Mac-Health-Check.zsh:1352–1680. - Pre-flight: create the log, sleep the jitter on LaunchDaemon runs, require root, secure the state directories, require
jq, install or refresh the client copy and LaunchDaemon (skipped for Test and Development), then upload the cached report and exit when the cache gate passed —Mac-Health-Check.zsh:6905–7059. - Outside
Silent: install or update swiftDialog and quit other swiftDialog windows —Mac-Health-Check.zsh:7206–7219. - Build
combinedJSONper vendor (or the Development subset); in Self Service, a recent unhealthy full-run baseline narrows the run to the failed checks plus inventory, and a healthy one replays the cached Inspect summary and exits —Mac-Health-Check.zsh:10927–10989. - Launch swiftDialog through the Dock-named copy, with fallback to
dialogcli—Mac-Health-Check.zsh:11026–11093. - Run the checks through
runConfiguredHealthCheck; Test mode marks every itemsuccesswithout running checks —Mac-Health-Check.zsh:11101–11483. quitScript(): compute overall status, send webhooks (not on nightly runs), generate, merge and lock-write the report, upload to HEC, write or launch the Inspect summary, run the countdown, clean up and exit —Mac-Health-Check.zsh:6702–6866.
Footguns
- Unescaped values in dialog JSON —
Mac-Health-Check.zsh:1628,Mac-Health-Check.zsh:1664—${ssid},${computerName}and${loggedInUserFullname}go raw into a JSON string; one"aborts the run in every mode (see S1). - Missing
jqreported as invalid JSON —Mac-Health-Check.zsh:1676— with no root-ownedjq,validateJsonfails becausejqis not found, so the script prints “mainDialogJSON is invalid JSON” to stdout and exits before the explicitjqfatal atMac-Health-Check.zsh:7009and before anything is written toscriptLog. - Unvalidated Parameter 4 —
Mac-Health-Check.zsh:105— any value other than the five names takes every!= "Silent"branch: it shows the dialog, writes the canonical report, uploads when Splunk is in production and installs the client copy (Mac-Health-Check.zsh:7041), but skips Self Service-only logic. Documented in README, not enforced. - Network Quality false success —
Mac-Health-Check.zsh:10346–10347— whennetworkQualityfails,dl_throughputis empty,bcprints “Parse error: bad expression” to stderr (observed in isolation),mbpsis empty, and the list item is stillstatus: success; the bad result file is reused for 4 hours. - Home-directory parse —
Mac-Health-Check.zsh:985—dsclprints a value containing a space on its own line, soawk '{print $2}'returns a blank line plus the last word (isolated check:dscl . read /Users/root RealNamegives[]then[Administrator]). OneDrive, Homebrew, App Auto-Patch and Entra checks then read the wrong path. - Shared swiftDialog log removed —
Mac-Health-Check.zsh:6859—rm -f /var/tmp/dialog.logalso removes the log of any other swiftDialog-based tool running at the same time.
Edge cases not handled
- Nightly LaunchDaemon run hits an early exit (invalid dialog JSON, missing
jamf.log, missingjq) → messages go to stdout, which the plist sends to/dev/null; no log entry —Mac-Health-Check.zsh:4322–4325. - External check exceeds 120 s →
captureCommandOutputWithTimeout()sends TERM, then KILL after 5 s, to the subshell runningjamf(Mac-Health-Check.zsh:2418–2426); a CrowdStrike check killed mid-run may leave the systemAppleLocaleaten_USif itsEXITtrap never runs (inferred; signal delivery to Jamf’s child script not reproduced). - Mosyle plus a webhook →
computerMdmURLstays empty, so the Slack buttonurlis"", which Slack rejects; the README already notes webhooks are effectively Jamf Pro-only —webHookMessage(). - Dock integration on → the ad hoc re-sign drops swiftDialog’s Team ID, so PPPC and notification profiles keyed to it do not match (documented in README).
Refactor suggestions
R1 — JSON-encode the help message · Quick win
Mac-Health-Check.zsh:1664
# before
"helpmessage" : "'"${helpmessage}"'",
# after (compute before mainDialogJSON)
helpmessageJSON=$( printf '%s' "${helpmessage}" | jq -Rs . )
…
"helpmessage" : '"${helpmessageJSON}"',
jq -Rs . emits a quoted, escaped JSON string (checked with /usr/bin/jq), which closes S1. Apply the same pattern to infobuttonaction and bannerimage if they ever become configurable.
R2 — Validate jq and root before the first JSON use · Quick win
Mac-Health-Check.zsh:76
# before
if [[ -n "${jqBinary}" ]] && [[ "${jqBinary}" != "/usr/bin/jq" ]]; then
function jq() { "${jqBinary}" "$@"; }
fi
# after
if [[ -z "${jqBinary}" ]]; then
echo "jq is required; install a root-owned jq (macOS 15+ ships /usr/bin/jq). Exiting."
exit 1
elif [[ "${jqBinary}" != "/usr/bin/jq" ]]; then
function jq() { "${jqBinary}" "$@"; }
fi
Reports the real cause instead of “mainDialogJSON is invalid JSON”. Also consider sending early-exit messages to scriptLog and pointing the LaunchDaemon’s StandardErrorPath at the log.
R3 — Reject unknown operation modes · Quick win
Mac-Health-Check.zsh:105
# before
operationMode="${4:-"Self Service"}"
# after
operationMode="${4:-"Self Service"}"
case "${operationMode}" in
"Debug" | "Development" | "Self Service" | "Silent" | "Test" ) ;;
* ) echo "Unrecognized operationMode '${operationMode}' (Parameter 4); exiting."; exit 1 ;;
esac
Fails closed on typos instead of running production behavior under an unknown mode. The client-copy sed at Mac-Health-Check.zsh:4251 matches only the assignment line, so it is unaffected.
R4 — Report Network Quality failures · Quick win
Mac-Health-Check.zsh:10346
# before
mbps=$( echo "scale=2; ( $dlThroughput / 1000000 )" | bc )
# after
if [[ "${dlThroughput}" != <-> ]]; then
rm -f "${networkQualityTestFile}"
dialogUpdate "listitem: index: ${1}, icon: SF=$(printf "%02d" $(($1+1))).circle.fill weight=bold colour=${statusColorError}, iconalpha: 1, status: error, statustext: Unable to measure"
warning "${humanReadableCheckName}: networkQuality returned no throughput"
return
fi
mbps=$( echo "scale=2; ( $dlThroughput / 1000000 )" | bc )
Stops reporting a failed measurement as healthy and drops the bad cache file (zsh <-> matches only non-empty integers, checked in isolation).
R5 — Parse the home directory from plist output · Quick win
Mac-Health-Check.zsh:985
# before
loggedInUserHomeDirectory=$( dscl . read "/Users/${loggedInUser}" NFSHomeDirectory 2>/dev/null | awk -F ' ' '{print $2}' )
# after
loggedInUserHomeDirectory=$( dscl -plist . read "/Users/${loggedInUser}" NFSHomeDirectory 2>/dev/null \
| plutil -extract 'dsAttrTypeStandard:NFSHomeDirectory.0' raw -o - - 2>/dev/null )
Handles values with spaces (checked with RealName on root, which returns System Administrator).
R6 — Grant Inspect assets to the console user only · Quick win
Mac-Health-Check.zsh:6176
# before
if ! chown root:wheel "${inspectFileToPrepare}" 2>/dev/null || ! chmod 644 "${inspectFileToPrepare}" 2>/dev/null; then
# after
if ! chown root:wheel "${inspectFileToPrepare}" 2>/dev/null || ! chmod 640 "${inspectFileToPrepare}" 2>/dev/null \
|| { [[ -n "${loggedInUser}" ]] && ! chmod +a "${loggedInUser} allow read" "${inspectFileToPrepare}" 2>/dev/null; }; then
Mirrors the dialog-file ACL pattern and closes S4; replay already regenerates for a different user (Mac-Health-Check.zsh:6389).
Quick wins vs deeper work
- Quick wins (< 1 hr): R1, R2, R3, R4, R5, R6.
- Deeper work: S2 evidence tagging (report schema and Splunk dashboard change) and S3 installer verification (needs testing against current swiftDialog releases).
Cross-cutting notes
Cross-cutting notes: one unescaped value can silently stop nightly compliance reporting
- S1 plus the nightly job’s discarded output. The JSON abort happens before logging is set up, and the LaunchDaemon sends stdout and stderr to
/dev/null, so a Mac on a network whose name contains"stops refreshing its compliance report with no local trace. The cached report then ages out (36 hours) and Silent uploads fall back to full runs that fail the same way. Splunk staleness alerts are the only signal; R1 and R2 together remove both halves. - Prior hardening is real. The 5.0.0 security changes claimed in
CHANGELOG.mdandSECURITY.md(root-owned state, parameter-secret rejection, trusted self-copy,/usr/local/binremoval, wrappermktemp -d, Electron content validation,Not Runningtreated as failure) were each confirmed in code. Earlierinstall.log-based or/var/tmp-based weaknesses now remain only as the fallbacks in S2. - Unattended schedule. The nightly job can run
networkQualityandsoftwareupdate --liston whatever network the laptop is on; the ±30-minute deterministic jitter spreads fleet load but not across networks. Not a defect; worth knowing for bandwidth planning.
Scope
Scope: 174b061 (GitHub repo) — 21 code/config files; main script about 56% read directly
| Field | Value |
|---|---|
| Date | 2026-10-04 19:10 EDT |
| Target | https://github.com/dan-snelson/Mac-Health-Check (GitHub repo, B) |
| Ref | 174b061fbbb9ad0bc8461dd9035651914eda436d (main, v5.0.0) |
| Generated by | Claude Opus 5.5 (claude-opus-5-5) |
| Language(s) | zsh (main script, wrapper, postinstall, two external checks), bash (ten external checks), Make, GitHub Actions YAML, osquery YAML |
| Files analyzed | 21 — Mac-Health-Check.zsh, Resources/createSelfExtracting.zsh, Resources/postInstall.zsh, Resources/Makefile, 12 scripts in external-checks/, Skills/mac-health-check-selector/scripts/build-artifact.zsh, .github/workflows/security-scan.yml, Resources/mac-health-check-fleet-policies.yml, AGENTS.md, CLAUDE.md |
| Automated scan | semgrep 1.177.0 — p/r2c-security-audit, p/secrets, p/ci — 0 results (0 confirmed), 7 errors on one file, 13 files skipped (12 PNGs over 1 MB, 1 parse failure) |
| Scope caveats | See below |
Scope caveats:
- Oversized main script.
Mac-Health-Check.zshis 11,491 lines; about 56% (6,463 lines) was read directly.- Read directly: 1–1659, 1672–1680, 2150–2989, 3700–4389, 5726–5760, 6040–7247, 7270–7349, 7586–8195, 8546–8612, 8840–8900, 8960–9000, 10127–10466, 10470–10649, 10840–11491.
- Pattern-scanned only (privileged commands, network, temp paths,
chown/chmod,eval,(P),log show,install.log): 1660–2149 (list-item JSON), 2990–3699 (report JSON build and merge), 4390–6039 (Inspect builders), 7350–7585 (checkOS()remainder,checkStagedUpdate()), 8196–8545 (SIP, SSV, Gatekeeper, firewall, uptime, disk), 8613–8839 (memory pressure, user directories, MDM profile), 8901–8959 and 9001–10126 (Entra, APNs, network hosts, MDM certificate, Jamf check-in and inventory, clock skew, Mosyle, FileVault, Touch ID, Wi-Fi, VPN), 10650–10839 (Bluetooth, password hint, AirPlay, AirDrop).
- Other files. All 12 external checks, the wrapper,
postinstalland Makefile were read in full.build-artifact.zsh(maintainer and admin tooling, not scored), the CI workflow and the Fleet policy YAML were read in part and pattern-scanned. Agent configuration,SECURITY.md, the 5.0.0CHANGELOG.mdsection and relevant README sections were read for claims. - Semgrep. No zsh parser; zero results is not evidence of safety. The 7 errors are
p/ciparse failures on embedded shell insecurity-scan.yml. - Isolated checks (no target code run).
ps -axww -o user=,args=as uid 502 listed root process arguments (Darwin 25.6, macOS 26.7.1);dscl . read /Users/root RealNamepiped to the script’sawkpattern;bcwith an empty operand;jq -Rs .,plutil -extract … -and the zsh<->pattern for refactor snippets. - Not reproduced. S1 injection and abort, S3
spctloutput for a forged certificate, and the CrowdStrike locale edge case were traced from code only. - Ownership data. Shallow clone (50 commits); contributor counts come from the GitHub API.
Attestation:
monocle_attestation: v1
skill_repo: https://github.com/dan-snelson/Monocle
skill_commit: unknown
skill_ref: copied install
skill_tree: unavailable
skill_sha256: c2bb20f34568f6fe0103705ff21aa1151781dc886a9526819476d724ceaf51a3
skill_dirty: unknown
target: https://github.com/dan-snelson/Mac-Health-Check (GitHub repo, B)
target_ref: 174b061fbbb9ad0bc8461dd9035651914eda436d (main, v5.0.0)
score_raw: 80
score_final: 80
score_alt: 72
band: Good
generated_by: Claude Opus 5.5 (claude-opus-5-5)
timestamp: 2026-10-04 19:10 EDT
Microsoft 365 Reset
Monocle Report: Microsoft-365-Reset
Monocle Score
Monocle Score: 94/100 (🟢 Excellent) — Approve with conditions
Score: 94/100 (🟢 Excellent) at the documented-deployment baseline — 4 Info, 3 non-security, or 91/100 (🟢 Excellent) if a user-facing policy runs test or debug mode with a blank $5; up from 60 at 93629a5
Overall risk: 🔵 Low
Recommendation: Approve with conditions — no security findings affect the score; before rollout, limit every user-facing Self Service policy to named actions and grant the management agent Full Disk Access.
Generated by: Claude Opus 5.5 (claude-opus-5-5) · Skill: monocle
| Source | IDs | Count | Each | Deduction |
|---|---|---|---|---|
| 🔴 Critical | — | 0 | 40 | 0 |
| 🟠 High | — | 0 | 20 | 0 |
| 🟡 Medium | — | 0 | 8 | 0 |
| 🔵 Low | — | 0 | 3 | 0 |
| ⚪ Info | S1, S2, S3, S4 | 4 | 0 | 0 |
| Non-security | Outlook reset removes Zoom and Webex plugin data; “Exchange” keychain label deletion; skipped operations unreported after a failed remove_office preinstall |
3 | 2 (max 20) | 6 |
| Not scored | — | 0 | 0 | 0 |
Total: 100 − 6 = 94; within the Low/Info range (70–100), no cap or floor applied → 94/100 (🟢 Excellent)
If a user-facing policy runs test or debug with a blank $5, S1 becomes Low: 100 − 9 = 91, within the Low range (70–100) → 91/100 (🟢 Excellent).
Score change: +34 from 60 at 93629a5 (the prior report used the same weights, so no recomputation was needed).
- Closed: prior S1 (root deletion under the home folder separated from its symlink check; deletions there now run as the console user), S2 (reset operations deleted
/Library/Managed Preferencesfiles), S3 (package signer checks ignored verification status), S4 (non-HTTPSFullUpdaterLocationaccepted), S5 (rootsqlite3write to a keychain path from the user’s home), the roll-up Low (unpinned Semgrep in CI), and all six prior non-security issues (substringpkill, root${TMPDIR}cleanups, home-path parsing, Teams backgrounds staged in/private/tmp, app removed before a failed install,launchctlcalls that missed their target). - Persisting: prior S7 (now S1), S6 (now S2), and S8 (now S4), all Info.
- New:
- S3 (Info) is the residue of prior S2: the Skype for Business line was already in that finding’s location list, and it is now rated against the 2.0.1 documentation claim.
- All three non-security issues were previously missed: the code was already present at
93629a5(git show 93629a5:Microsoft-365-Reset.zshlines 1914, 1944, 2467, and the preinstall exit path described in the prior Control flow).
Operator baseline:
- Every user-facing Self Service policy sets an explicit
$5operations allowlist, and$6(--allow-all-operations) is set only on admin-only policies. The code enforces this inself-servicemode (exit10), so it is a gate, not a finding. - User-facing policies don’t use
testordebugmode (S1 becomes Low if one does with a blank$5). - Silent policies list only the intended operations and are scoped deliberately, because silent mode skips the destructive confirmation by design (documented).
Executive View
Executive View: Approve with conditions — prior risks fixed; needs allowlists and disk access
In one sentence: Gives the service desk and Mac users a guided, self-service way to repair, reset, or remove Microsoft 365 apps on a Mac, replacing Microsoft’s older reset packages with one maintained tool.
- No significant security problems remain. This release fixes every security issue the previous review raised, including the timing gap that could let a signed-in user steer a deletion outside their own folder and the removal of IT-managed Office settings during routine resets.
- Actions close Microsoft apps without saving, and some permanently delete local mail, notes, and sign-ins. Interactive runs warn the user and ask for confirmation; unattended runs do not, by design. Apps can be reinstalled, but deleted local data can’t be recovered without backups.
- Administrators choose which actions each policy offers. The tool refuses to show its menu to users unless the policy names the allowed actions, so removing Office or the security agent appears only where IT deliberately allows it.
- One new setup step is needed before rollout. The device-management agent must be granted Full Disk Access. Without it, macOS blocks some cleanup, and the tool now reports those actions as failed instead of claiming success.
- A few cleanup steps reach slightly beyond their labels. Resetting Outlook also removes Zoom and Webex meeting add-in data, and resetting sign-ins removes any saved password labelled “Exchange”, which another mail app may also use.
- One person maintains the tool, and testing is limited to automated code checks.
Monocle Score: 94/100 (🟢 Excellent), or 91/100 (🟢 Excellent) if a user-facing policy uses the tool’s test or debug mode without naming the allowed actions
Recommendation: Approve with conditions — no security findings affect the score; before rollout, limit every user-facing Self Service policy to named actions and grant the management agent Full Disk Access.
Security View
Security View: 🔵 Low risk — no scored findings (4 Info)
Execution context: root via Jamf Pro policy or sudo (Microsoft-365-Reset.zsh:2912). Every deletion, move, and keychain-database edit under the console user’s home or temp folder runs as the console user through launchctl asuser … sudo -u (Microsoft-365-Reset.zsh:336), as do keychain, defaults, open, tccutil, and restart calls. swiftDialog runs as root from a validated path.
Overall risk: 🔵 Low — there are no Critical, High, Medium, or Low findings; the four Info items are documented, admin-gated behavior or documentation wording, and every scored finding from the prior review is closed in code.
Findings
| ID | Severity | Title | Location |
|---|---|---|---|
| S1 | ⚪ Info | test and debug modes show every operation when the allowlist is blank |
Microsoft-365-Reset.zsh:1122–1129 |
| S2 | ⚪ Info | remove_office deletes the shared /Library/Logs/Microsoft folder (documented, admin-gated) |
Microsoft-365-Reset.zsh:1593 |
| S3 | ⚪ Info | remove_skypeforbusiness deletes a managed preference that the docs attribute only to remove_office |
Microsoft-365-Reset.zsh:2694 |
| S4 | ⚪ Info | Repo ships agent instructions that set a chat style only | AGENTS.md:7–10 |
S1 — test and debug modes show every operation when the allowlist is blank · ⚪ Info
- Location:
showSelectionDialog(),Microsoft-365-Reset.zsh:1122–1129; the allowlist gate atMicrosoft-365-Reset.zsh:2947 - Origin: Deployment — the default
self-servicemode fails closed without$5(exit10);testanddebugare documented operator modes (README.md:125) that only log aWARNING. - Evidence:
warning "No --operations / \$5 allowlist supplied; showing all ${#operationIDs[@]} operations (${operationMode} mode)" - Impact: None at the documented baseline. If a user-facing policy sets
$4totestordebugand leaves$5blank, users see every operation, includingremove_officeandremove_defender. That would be a Low secure-default gap (Rule 14), anddebugwould also write a fullset -xtrace (Microsoft-365-Reset.zsh:111) into the Jamf policy log. - Fix: Optional: require the same
$6opt-in intestanddebugby changing the gate at line 2947 from"${operationMode}" == "self-service"to"${operationMode}" != "silent".
S2 — remove_office deletes the shared /Library/Logs/Microsoft folder · ⚪ Info
- Location:
removeOfficePreinstall(),Microsoft-365-Reset.zsh:1593 - Origin: Code + Deployment — documented (
README.md:18,README.md:383), MOFA-aligned, and gated by the allowlist and the interactive destructive confirmation (Microsoft-365-Reset.zsh:1243–1279). - Evidence:
safeRemove "/Library/Logs/Microsoft" - Impact: Defender and Intune local logs are lost after a full removal. This is documented, intended behavior (Rule 14). The parent
/Library/Application Support/Microsoftis preserved; only the Office-owned children are removed (Microsoft-365-Reset.zsh:1561–1563). - Fix: None required. Optionally narrow the deletion to Office-owned log children in a later release.
S3 — remove_skypeforbusiness deletes a managed preference that the docs attribute only to remove_office · ⚪ Info
- Location:
op_remove_skypeforbusiness(),Microsoft-365-Reset.zsh:2694 - Origin: Code — documentation wording.
README.md:50says “onlyremove_office” removes profile-delivered managed preferences, and the operation’s row (README.md:152) doesn’t mention them. - Evidence:
safeRemove "/Library/Managed Preferences/com.microsoft.SkypeForBusiness.plist" - Impact: Negligible. The same operation removes the Skype for Business app, and
remove_officealways adds it (Microsoft-365-Reset.zsh:1222), where managed-preference removal is documented. macOS regenerates the file from an installed profile (inferred). - Fix: Add “managed preferences” to the
README.md:152row, or drop the line if the app’s removal is enough.
S4 — Repo ships agent instructions that set a chat style only · ⚪ Info
- Location:
AGENTS.md:7–10,.github/copilot-instructions.md:1–2 - Origin: Code — contributor tooling. There are no hooks that run on agent session start (
.codex/config.tomlsets only a credential-store option, and no.claude/directory exists), and no text addressed to reviewers. - Evidence:
Respond terse like smart caveman. All technical substance stay. - Impact: None. Benign agent tooling, ignored for this report under Rules 5 and 12.
- Fix: None.
Low / Info roll-up: No unscored gaps. Credited, verified in code:
PATHpinned without/usr/local/bin(Microsoft-365-Reset.zsh:28).- Deletions under the home and per-user temp folders run as the console user with no root fallback (
Microsoft-365-Reset.zsh:392–394), after a parent-symlink check that keeps the canonical parent under the canonical home (Microsoft-365-Reset.zsh:343–365). - Teams backgrounds are archived, staged, and restored as the console user, and root checks the staging folder’s name, type, and owner (
Microsoft-365-Reset.zsh:2283,Microsoft-365-Reset.zsh:2297–2302). - The keychain-database lookup uses
-type fand bothsqlite3edits run as the console user (Microsoft-365-Reset.zsh:524,Microsoft-365-Reset.zsh:2218,Microsoft-365-Reset.zsh:2649); theOffice365V2rename runs as the console user (Microsoft-365-Reset.zsh:2589). pkgutil --check-signaturemust exit 0 and report an Apple-issued distribution certificate (Microsoft-365-Reset.zsh:653–654); the swiftDialogspctlassessment must exit 0 before the Team ID is compared (Microsoft-365-Reset.zsh:946–948).- Custom MAU channels require
https://for bothManifestServerandFullUpdaterLocation(Microsoft-365-Reset.zsh:815,Microsoft-365-Reset.zsh:823–826). - Repairs stage packages in a root-private
mktemp -d, check type and root ownership, move the original bundle aside only after verification, and restore it when the install or the post-installcodesigncheck fails (Microsoft-365-Reset.zsh:696–787). - swiftDialog is resolved into
Dialog.app, ownership and mode are checked up the tree, the signature is checked against Team IDPWA5E9TQ59, and the version is re-read after an upgrade (Microsoft-365-Reset.zsh:955–988,Microsoft-365-Reset.zsh:1018). cfprefsdis restarted only for the console user and root (Microsoft-365-Reset.zsh:2658–2659); launchd jobs are booted out by label in the correct domain (Microsoft-365-Reset.zsh:428–435).- The per-user temp folder is accepted only under
/var/folders, not a symlink, and owned by the user (Microsoft-365-Reset.zsh:2936–2945). - Root deletions of fixed names under
/Users/Shared(Microsoft-365-Reset.zsh:1735,Microsoft-365-Reset.zsh:2015) remove a planted link, not its target (isolated check; see Scope caveats). - The wrapper generator extracts into a private
mktemp -dand forwards"$@"(Resources/createSelfExtracting.zsh:41–48); CI pins every Action to a commit SHA, setspermissions: read-all, and pins Semgrep to1.179.0(.github/workflows/security-scan.yml:18,.github/workflows/security-scan.yml:61).
Privilege map
- Root
rm -rfof system paths —Microsoft-365-Reset.zsh:396— root rm -rfof home and per-user temp paths —Microsoft-365-Reset.zsh:394— console user- Package installs (
installer -pkg … -target /) —Microsoft-365-Reset.zsh:764,Microsoft-365-Reset.zsh:950— root - Move an app bundle aside and restore it —
Microsoft-365-Reset.zsh:756,Microsoft-365-Reset.zsh:780— root pkgutil --forgetof Microsoft, Skype, DLP, Zoom, and WebEx receipts —Microsoft-365-Reset.zsh:1721,Microsoft-365-Reset.zsh:2704–2705,Microsoft-365-Reset.zsh:2746–2748,Microsoft-365-Reset.zsh:2812,Microsoft-365-Reset.zsh:2839— root- System preference writes (
defaults write /Library/Preferences/com.microsoft.autoupdate2) and the licensing-plist rename —Microsoft-365-Reset.zsh:1748,Microsoft-365-Reset.zsh:2505,Microsoft-365-Reset.zsh:2521,Microsoft-365-Reset.zsh:2582— root launchctl bootoutandbootstrapof Microsoft, Zoom, and WebEx jobs —Microsoft-365-Reset.zsh:433,Microsoft-365-Reset.zsh:444— root- Vendor uninstallers run from
/Applications(Defender, Zoom, WebEx) —Microsoft-365-Reset.zsh:2720,Microsoft-365-Reset.zsh:2796,Microsoft-365-Reset.zsh:2820— root - Force-quit of Microsoft and Adobe processes (
pkill -9, exact names or anchored prefixes) —Microsoft-365-Reset.zsh:1523–1535— root killall cfprefsdfor the console user and root —Microsoft-365-Reset.zsh:2658–2659— root- Keychain deletions,
sqlite3keychain-database edits, Teams background andOffice365V2moves,tccutil reset,defaultswrites,open, restart Apple event —Microsoft-365-Reset.zsh:567,Microsoft-365-Reset.zsh:2218,Microsoft-365-Reset.zsh:2283,Microsoft-365-Reset.zsh:2589,Microsoft-365-Reset.zsh:2357,Microsoft-365-Reset.zsh:2609–2619,Microsoft-365-Reset.zsh:1416— console user vialaunchctl asuser
Secrets inventory
No secrets observed. Jamf Parameters $4–$6 carry only the mode, the operation list, and the allow-all flag (Microsoft-365-Reset.zsh:55–57). Keychain deletions discard security output (Microsoft-365-Reset.zsh:567–570) and suspend xtrace (Microsoft-365-Reset.zsh:559), so debug mode’s set -x (Microsoft-365-Reset.zsh:111) does not trace them.
Network egress
go.microsoft.com/fwlink/?linkid=…→ Microsoft CDN —nscurlHEAD and download — HTTPS, unauthenticated; Apple distribution status and Microsoft signer checked —Microsoft-365-Reset.zsh:661–675,Microsoft-365-Reset.zsh:722- MAU custom
ManifestServer(only withChannelName=Custom) —nscurlGET — HTTPS required for the server and forFullUpdaterLocation—Microsoft-365-Reset.zsh:815–828 api.github.com/repos/swiftDialog/swiftDialog/releases/latestand thegithub.comrelease asset —curl --failGET with connect and total timeouts — HTTPS, unauthenticated; Gatekeeper verdict and Team ID checked, signature requirement re-checked after install —Microsoft-365-Reset.zsh:924–948,Microsoft-365-Reset.zsh:981- Icon URLs on
usw2.ics.services.jamfcloud.comandswiftdialog.app— fetched by swiftDialog for display — HTTPS —Microsoft-365-Reset.zsh:40–43,Microsoft-365-Reset.zsh:229–247
Persistence
No new persistence. reset_autoupdate reloads the existing MAU agent and daemon from their root-owned plists after booting them out (Microsoft-365-Reset.zsh:2460–2462) and writes MAU application registrations that persist as intended configuration (Microsoft-365-Reset.zsh:2505–2540). remove_office removes Microsoft LaunchAgents and LaunchDaemons (Microsoft-365-Reset.zsh:1578–1586).
Manager View
Manager View: Production-grade; single maintainer (Dan K. Snelson)
Ownership: Dan K. Snelson — script header (Microsoft-365-Reset.zsh:15), SECURITY.md contact, and 54 of 54 commits in local history. Bus factor: 1.
Maturity: Production-grade — validates inputs and fails closed, now reports every path it could not remove as an operation failure, has predictable exit codes (0, 2, 10, 20), writes a timestamped log with a final summary line, and is versioned and thoroughly documented; testing is limited to syntax checks and static scans.
Change risk: High — runs as root, makes irreversible deletions (mail, notes, keychain items, security tooling), lives in one 3,068-line file with 106 functions, has no functional tests, and has a single maintainer.
What it depends on
- Full Disk Access for the management agent (a PPPC profile for the Jamf Pro binary) — new hard requirement in 2.0.1 (
README.md:85). Without it, cleanup inside app containers fails and each affected operation exits20. - swiftDialog 3.0.1.4955+ in
/Library/Application Support/Dialog/Dialog.app— interactive modes install or upgrade it from GitHub on demand. The unauthenticated GitHub API is rate-limited per public IP, so a large site behind one NAT can exit10before any dialog appears (inferred). - Microsoft
go.microsoft.comfwlink IDs and the Microsoft CDN — a retired link ID breaks repair for that app, reported as an operation failure (exit20). - Hard-coded minimum versions (Office 16.73, OneDrive 23154.0, Teams 23247.0, MAU 4.49) — need periodic review;
scripts/mofa-consult.zshchecks them against MOFA’s feed. - Organization-specific log path
/var/log/org.churchofjesuschrist.log(Microsoft-365-Reset.zsh:60) and Jamf-hosted icon URLs — must change for other environments. - Jamf Pro policies passing
$4–$6— the$5allowlist is the main safety control and isn’t visible in the code. - One maintainer’s local release helper (
.deployMicrosoft365Reset.zsh, untracked) — nobody else can cut a release without it.
Fragility hotspots
- Full Disk Access attribution. User-context deletions run through
launchctl asuser … sudo -u … rm. Whether macOS credits them to the Jamf binary’s Full Disk Access grant was not tested here (inferred). If it doesn’t, container cleanup fails on every Mac and shows up asWARNING Failed to remove pathlines and exit20—Microsoft-365-Reset.zsh:394,Microsoft-365-Reset.zsh:3046–3048. This is documented behavior and isn’t scored. - Outlook reset removes third-party plugin data.
reset_outlookdeletes the Zoom and Webex Outlook plugin support folders, which the README’sreset_outlookrow doesn’t mention (README.md:141). Users may find the Zoom or Webex add-in broken after an Outlook reset (inferred), and it would surface only as user reports —Microsoft-365-Reset.zsh:2013–2015. - “Exchange” keychain items.
reset_outlook,reset_license, andreset_credentialsdelete every generic keychain item labelled exactlyExchange, whichever app created it. Another mail client’s saved password may be removed (inferred), and users would simply be prompted to sign in again —Microsoft-365-Reset.zsh:2045,Microsoft-365-Reset.zsh:2574. - Failed
remove_officepreinstall hides skipped work. When the preinstall phase leaves an app bundle behind, the run exits20before any other selected operation runs (for example,remove_defender), and the summary lists onlyremove_officeas failed —Microsoft-365-Reset.zsh:3013–3025. - Version-string gating is not a concern: nothing caches on
scriptVersion, and the two commits since the prior review carry distinct versions (2.0.0,2.0.1).
Change & rollback
- Testing:
zsh -n, Semgrep (pinned), Gitleaks, and ShellCheck in CI (.github/workflows/security-scan.yml). No functional tests, andtestmode is not a dry run (documented,README.md:392). - Rollback: A failed auto-repair now restores the original app bundle. Other deletions can’t be undone: apps can be reinstalled and caches rebuild, but local mail, unsynced OneNote content, keychain items, Defender, and logs need backups or reinstallation (documented,
README.md:393). Prior script versions are available as GitHub release assets. - Deploy path: Jamf Pro policy (Self Service or silent) passing
$4–$6, or manualsudo; release assets are published by the local, untracked.deployMicrosoft365Reset.zsh.
Action items
| # | Action | Owner role | Effort | Priority |
|---|---|---|---|---|
| 1 | Deploy a Full Disk Access PPPC profile for the Jamf binary, then pilot every allowed operation on a test Mac and confirm no Failed to remove path warnings |
Jamf admin | S | P1 |
| 2 | Confirm every user-facing Self Service policy sets $5, $6 appears only on admin-only policies, and no user-facing policy uses test or debug (S1) |
Jamf admin | S | P1 |
| 3 | Move the Zoom and Webex plugin deletions out of reset_outlook, or document them in the operations table |
Mac engineer | S | P3 |
| 4 | Find out which apps write Exchange-labelled keychain items, then narrow the deletion or document it |
Mac engineer | S | P3 |
| 5 | Record operations skipped after a failed remove_office preinstall in the summary and completion dialog |
Mac engineer | S | P3 |
| 6 | Correct the managed-preference wording for remove_skypeforbusiness (S3) |
Mac engineer | S | P3 |
| 7 | Add a second maintainer and a VM-based smoke test of self-service and silent before each release |
Service owner | M | P3 |
Engineer View
Engineer View: Privilege model is sound; three inherited cleanup-scope gaps remain
Shape: zsh (#!/bin/zsh --no-rcs), 3,068 lines, 106 functions, single entry main "$@" (Microsoft-365-Reset.zsh:3068); invoked by Jamf ($1–$3 skipped, $4–$6 read) or by CLI flags that override them.
Inputs → Outputs: $4 mode, $5 operation CSV, $6 allow-all flag, --mode/--operations/--allow-all-operations, swiftDialog JSON selections, MAU preferences, Microsoft and GitHub HTTP responses → deletions across /Applications, /Library, and the console user’s home and temp folders; package installs; pkgutil receipt removal; keychain deletions; MAU registration prefs; /var/log/org.churchofjesuschrist.log; exit 0 / 2 / 10 / 20.
Control flow
- Pin
PATH, read$4–$6defaults, then parse CLI flags; leading positionals are skipped until the first flag, and later bare positionals exit10—Microsoft-365-Reset.zsh:28,Microsoft-365-Reset.zsh:55–104. - Normalize the mode (
debugturns onset -x; an unknown mode exits10); allow-all accepts onlytrue/yes/1—Microsoft-365-Reset.zsh:106–132. - Create a root
mktemp -dwork directory and a root-owned0644command file;trap cleanup EXITlogs the summary and removes both —Microsoft-365-Reset.zsh:136–141,Microsoft-365-Reset.zsh:278–289. preflightChecks: require root and a real console user; resolve the home throughdscl, thendscacheutil, then/Users/<name>; reject/and/var/root; accept the per-user temp folder only if trusted; enforce theself-serviceallowlist (exit10); interactive modes rundialogCheck—Microsoft-365-Reset.zsh:2906–2954.- Intro dialog (interactive only); cancel exits
0—Microsoft-365-Reset.zsh:1035–1056,Microsoft-365-Reset.zsh:2971–2977. - Selection: silent parses the CSV; interactive builds checkboxes from the validated allowlist, or from every operation, and re-prompts until one is selected —
Microsoft-365-Reset.zsh:1101–1174. - Validate IDs, expand dependencies and suppressions, and sort into phases (resets → data removal → ancillary removals →
remove_office) —Microsoft-365-Reset.zsh:2956–2966,Microsoft-365-Reset.zsh:1176–1241,Microsoft-365-Reset.zsh:2881–2904. - Destructive confirmation for
remove_office,remove_outlook_data,remove_onenote_data, andremove_defenderin interactive modes; an unacknowledged checkbox exits2—Microsoft-365-Reset.zsh:1243–1279. - Start the progress dialog in the background, reading the command file —
Microsoft-365-Reset.zsh:1281–1311. - If
remove_officeis selected, run its preinstall teardown first; if app bundles remain, show the completion dialog and exit20—Microsoft-365-Reset.zsh:3006–3030. - Dispatch each operation with
removalFailuresreset per operation; any pathsafeRemovecouldn’t remove fails that operation —Microsoft-365-Reset.zsh:3032–3055,Microsoft-365-Reset.zsh:375–407. - App resets call
maybeRepairOfficeApp→repairFromMicrosoftPkg(download to a root-private directory, verify, move the original aside, install,codesign, restore on failure) and return2to defer cleanup after a repair —Microsoft-365-Reset.zsh:834–910,Microsoft-365-Reset.zsh:677–794. - Finish the progress dialog, show completion (a warning icon on failure), offer a restart in interactive modes, then exit
20if any operation failed, otherwise0—Microsoft-365-Reset.zsh:3057–3065.
Footguns
- Outlook reset reaches into third-party plugins —
Microsoft-365-Reset.zsh:2013–2015—op_reset_outlook()deletes/Library/Application Support/Microsoft/WebExPlugin,…/ZoomOutlookPlugin, and/Users/Shared/ZoomOutlookPluginon every reset with no repair, althoughremove_zoompluginandremove_webexptexist for that. Inherited verbatim from MOFA and the package-eraOffice_Outlook_Resetpostinstall (lines 194–196); parity doesn’t change the scope. - Label-only keychain match —
Microsoft-365-Reset.zsh:2045,Microsoft-365-Reset.zsh:2574—deleteGenericByLabelLoop 'Exchange'deletes every generic password whose label is exactlyExchange, with no service or creator constraint. Any non-Microsoft item with that label is removed too (inferred; which apps write it wasn’t checked). Inherited from the package-era Outlook and credentials resets.
Edge cases not handled
remove_officepreinstall leaves a bundle (for example, one blocked by TCC or a root-owned leftover) →exit 20before the other resolved operations run, and the summary and completion dialog name onlyremove_office—Microsoft-365-Reset.zsh:3013–3025.
Refactor suggestions
R1 — Leave third-party plugin data to its own operations · Quick win
Microsoft-365-Reset.zsh:2013–2015
# before
safeRemove "/Library/Application Support/Microsoft/WebExPlugin"
safeRemove "/Library/Application Support/Microsoft/ZoomOutlookPlugin"
safeRemove "/Users/Shared/ZoomOutlookPlugin"
# after
# Zoom and Webex plugin data are removed only by remove_zoomplugin / remove_webexpt
Keeps reset_outlook within its documented scope; record the divergence in the MOFA parity notes. If the deletion stays, add it to the README.md:141 row instead.
R2 — Report operations skipped by a failed remove_office preinstall · Quick win
Microsoft-365-Reset.zsh:3013–3015
# before
if ! removeOfficePreinstall; then
appendFailure remove_office
errorOut "remove_office preinstall phase failed"
# after
if ! removeOfficePreinstall; then
appendFailure remove_office
errorOut "remove_office preinstall phase failed"
local skippedOp
for skippedOp in "${resolvedOperations[@]}"; do
[[ "${skippedOp}" == "remove_office" ]] && continue
appendFailure "${skippedOp}"
warning "Skipped ${skippedOp}: remove_office preinstall failed"
done
The exit 20 already holds; this makes the summary line and the completion dialog’s failed-ID list complete.
Quick wins vs deeper work
- Quick wins (< 1 hr): R1, R2; the S1 gate change (line 2947); the S3 README wording; for the
Exchangeloop, log the matched item’s service and creator (never its account) on a test Mac, then add-sor-Gto the delete once the Outlook value is known. - Deeper work: a VM-based smoke test that runs each allowed operation under the production PPPC profile, because the 2.0.1 user-context deletion model depends on how macOS attributes Full Disk Access through
launchctl asuser … sudo.
Cross-cutting notes
Cross-cutting notes: Full Disk Access is now a hard prerequisite; 2.0.1 claims verified
- Security fix traded for an operational dependency. Moving home-folder deletions into the console user’s context closes the prior check-then-delete race, but those deletions are now subject to macOS privacy protections. Together with the 2.0.1 breaking change (an unremoved path fails the operation), a missing or ineffective PPPC profile makes resets fail loudly (exit
20) across the fleet instead of silently succeeding. That is the right failure mode, but pilot the PPPC profile before rollout (Manager action 1). - Verified 2.0.1 claims. Each security claim in
CHANGELOG.md:7–13andSECURITY.md:44,SECURITY.md:51–53holds in code: user-context deletion without root fallback (Microsoft-365-Reset.zsh:392–394), reset operations leaving/Library/Managed Preferencesalone (Microsoft-365-Reset.zsh:1786), thepkgutilexit and status check (Microsoft-365-Reset.zsh:653–654),https://forFullUpdaterLocation(Microsoft-365-Reset.zsh:823–826), user-context keychain-database edits andOffice365V2rename (Microsoft-365-Reset.zsh:2218,Microsoft-365-Reset.zsh:2589), move-aside and restore (Microsoft-365-Reset.zsh:747–787), and the swiftDialog version re-check (Microsoft-365-Reset.zsh:1018). One wording is broader than the code: “onlyremove_office” removes managed preferences (README.md:50) also excludesremove_skypeforbusiness(S3). - Maintainer tooling. Every not-scored item from the prior review is closed.
.deployMicrosoft365Reset.zshruns commands withouteval(line 148), writes release notes withmktemp -t(line 203), and stages withgit add -u(line 399).scripts/mofa-consult.zshdefaults its report to the per-user$TMPDIR(line 21), opens it with no-follow (line 709), and no longer pushes the maintainer’s fork by default (line 35).
Scope
Scope: 43bbf33 local path — main script read in full, tooling diff-reviewed
| Field | Value |
|---|---|
| Date | 2026-10-03 20:23 EDT |
| Target | /Users/dan/Documents/GitHub/dan-snelson/Microsoft-365-Reset (local path) |
| Ref | 43bbf338bf2643397fb7a45ce387515e05522b05 (main, clean tracked tree) |
| Generated by | Claude Opus 5.5 (claude-opus-5-5) |
| Language(s) | zsh (main script, helpers), POSIX sh (generated wrapper text), GitHub Actions YAML with embedded bash |
| Files analyzed | 5 code files — Microsoft-365-Reset.zsh, Resources/createSelfExtracting.zsh, scripts/mofa-consult.zsh, .github/workflows/security-scan.yml, .deployMicrosoft365Reset.zsh (untracked, local only); plus agent configuration and docs (see below) |
| Automated scan | semgrep 1.177.0 — p/r2c-security-audit, p/secrets, p/ci — 0 results (0 confirmed), 6 parse errors, 1 file skipped (analysis failure), gitignored local-only files not scanned |
| Scope caveats | See below |
Scope caveats:
- Coverage.
Microsoft-365-Reset.zshwas read in full (lines 1–3068) andResources/createSelfExtracting.zshin full (lines 1–86). Maintainer tooling, which is not scored except for security findings, was reviewed by diff and pattern scan:scripts/mofa-consult.zsh(852 lines): the full diff since93629a5plus a pattern scan..github/workflows/security-scan.yml(466 lines): lines 1–35 and 160–200, the diff since93629a5, and a pattern scan..deployMicrosoft365Reset.zsh(486 lines, untracked): lines 140–160, 196–206, and 392–402 plus a pattern scan.- All three were read in full by the prior report at
93629a5.
- Agent configuration (Rule 5). Scanned, not analyzed as code:
AGENTS.md(lines 1–20 and the diff since93629a5),.github/copilot-instructions.md,.github/agents/*.agent.md(3),.github/instructions/*.instructions.md(4), and.codex/config.toml(untracked). No session-start hooks and no reviewer-directed text. - Documentation checked against code.
README.md,CHANGELOG.md, andSECURITY.md. - Local-only, gitignored files.
.deployMicrosoft365Reset.zsh,.codex/config.toml,.envrc,VERSION.txt,.DS_Storefiles, andResources/Microsoft_Office_Reset_2.0.0b1.pkgwith its expanded tree. The expanded tree is Microsoft’s package-era reference; it isn’t deployed by this project and was used only for parity greps. - Semgrep detail. All 6 parse errors and the single analysis-failure skip are in
.github/workflows/security-scan.yml(embedded bash, a scanner limitation). Semgrep has no zsh parser, so zero results doesn’t mean a clean zsh codebase; every Step 4 check was done manually. - Parity references. The sibling MOFA checkout (
../MOFAatf78517a28a) was consulted read-only for deletion parity. - Isolated checks (Rule 4). Both on the analysis host, macOS 26.7.1, with no side effects outside the session scratch directory:
pgrep -xmatched a 17-character process name (accessoryupdaterd), so exact-name force-quits ofMicrosoft PowerPoint,Microsoft Outlook, andMicrosoft OneNoteare not truncated./bin/rm -rfon a symlink operand and on a directory containing a symlink removed only the links; the target file survived.
- Prior report.
$HOME/monocle-reports/monocle-microsoft-365-reset-2026-09-30-1200.md(ref93629a5) was used as a checklist only; every finding was re-verified against the current checkout, and the prior score used the same weights. - Inferences. Exploit and failure chains come from reading the code and were not reproduced. The TCC attribution of user-context deletions and the owners of
Exchange-labelled keychain items are inferred.
Attestation:
monocle_attestation: v1
skill_repo: https://github.com/dan-snelson/Monocle
skill_commit: 8f331dbad0453b06cb663090233cd6fd4e5dee5f
skill_ref: development
skill_tree: c5fed732c4efff199406c4c78e740d95091d9bcd
skill_sha256: c2bb20f34568f6fe0103705ff21aa1151781dc886a9526819476d724ceaf51a3
skill_dirty: true
target: /Users/dan/Documents/GitHub/dan-snelson/Microsoft-365-Reset (local path)
target_ref: 43bbf338bf2643397fb7a45ce387515e05522b05 (main, clean tracked tree)
score_raw: 94
score_final: 94
score_alt: 91
band: Excellent
generated_by: Claude Opus 5.5 (claude-opus-5-5)
timestamp: 2026-10-03 20:23 EDT
Self-improvement
Monocle includes two, optional, maintenance passes, helping the skill become sharper after each run (and hopefully without getting bloated):
post-chat-refine
Every report generation ends with a one-line offer to run post-chat-refine. Once manually approved, Monocle folds that run’s durable, widely applicable lessons back into SKILL.md and its references/.
Before anything is written, Monocle sanitizes target names, organizations, paths, and other identifying details so each lesson reads as general guidance.
Note: Edits land in-place; review the diff before committing.
binge-and-purge
Each refine pass makes SKILL.md a little larger (the “binge”). This pass is the counterweight (the “purge”): it moves conditional content into references/ without changing meaning, so SKILL.md still fits in a single 25,000-token Read.
Run it on-demand; Monocle offers it when headroom drops below about 3,000 tokens.
Related Posts
Support
Best-effort support is available on the Mac Admins Slack (free, registration required) — sans a dedicated channel for this script — or you can open an issue (after reviewing the README.md).