Menu Close

Monocle: Initial Commit

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

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.

ViewForAnswers
👔 ExecutiveLeadership, change boardsWhat it does, business risk, and a clear recommendation (up to 6 bullets)
🔒 SecuritySecurity reviewersSeverity-ranked findings, privilege map, secrets, network, and persistence
📋 ManagerTeam leads, service ownersOwnership, fragility, change risk, and prioritized action items
🛠️ EngineerMaintainers, reviewersControl flow, footguns, edge cases, and before/after refactors with line references

📈 Monocle Score

Every report starts at 100 and loses points for distinct issues:

IssueDeduction
🔴 Critical / 🟠 High / 🟡 Medium / 🔵 Low Security finding40 / 20 / 8 / 3
⚪ Info Security finding0
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

  1. Visit the Monocle releases page
  2. Download both the .zip and the .zip.sha256 from the latest release
  3. In Terminal, cd ~/Downloads and validate the downloaded .zip
    • For example: shasum -a 256 -c Monocle-initial-commit.zip.sha256
  4. Only if you observe OK, expand the .zip
    • For example, unzip Monocle-initial-commit.zip
  5. Validate the unpacked skill’s SHA256 matches the skill_sha256 hash displayed on the release page
    • For example, python3 monocle/scripts/verify_report.py --manifest monocle
  6. Only after you’ve confirmed a matching skill_sha256, install the skill:
    1. 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/
    2. Codex
      • mkdir -pv ~/.agents/skills (as needed)
      • rm -Rfv ~/.agents/skills/monocle (if upgrading, remove the old directory first)
      • cp -Rv monocle ~/.agents/skills/
  7. 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

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.zsh
  • monocle review ./postinstall
  • Give 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:

  • PastDeadlineRestartBehavior stays at its default Off, or Force is used only on macOS versions where /var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist exists (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 into Resources/JamfEA-Pending_OS_Update_Date.zsh:712 and Resources/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-owned SoftwareUpdateDDMStatePersistence.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 a softwareupdated[pid]: tag, which logger -t softwareupdated -i produces).
  • 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 by softwareupdated” (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 in SECURITY.md.

S2 — CSV formula injection in the Jamf Pro status report · 🔵 Low

  • Location: Resources/Jamf-getDDMstatusFromCSV.zsh:898 (sanitizeForCsv), written at Resources/Jamf-getDDMstatusFromCSV.zsh:3831, opened at Resources/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 a HYPERLINK lure 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 (cleanup EXIT trap), Resources/Jamf-getDDMstatusFromCSV.zsh:437 and Resources/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), but die() paths such as Resources/Jamf-getDDMstatusFromCSV.zsh:3137 exit through cleanup, which never calls invalidateBearerToken.
  • Evidence: die "Failed to process CSV file."; apiUrl="${userInput}" with no https:// 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 invalidateBearerToken from cleanup; reject any apiUrl that 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.plist is missing or unrecognized, the code falls back to trusting install.log text, logs a [WARNING], and SECURITY.md:45 documents this as accepted residual risk.
  • Evidence: [[ "${ddmStatePersistenceStatus}" == "available" ]] || return 0
  • Impact: At the documented default (PastDeadlineRestartBehavior Off), a forged past-due declaration only produces false reminders and wrong EA inventory, as SECURITY.md states. If an admin sets Force, 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.md doesn’t mention this. 🔵 Low in that configuration.
  • Fix: Never enter Force mode from an uncorroborated declaration (downgrade to Prompt), and extend SECURITY.md:45 to say so.

Low / Info roll-up:

  • Credit: PATH pinned to system directories and swiftDialog called by its root-owned bundle path (reminderDialog.zsh:20, launchDaemonManagement.zsh:41); per-run mktemp -d dialog directory with guarded cleanup (reminderDialog.zsh:4348); atomic mktemp + mv -f replacement of the runtime script, starter, fallback plist, and LaunchDaemon with ownership verified by stat (launchDaemonManagement.zsh:603, launchDaemonManagement.zsh:887); chown -h on owned assets; console-user language sanitized with an allowlist regex before it reaches ${(P)}/printf -v (reminderDialog.zsh:571); swiftDialog pkg checked with spctl -a -vv -t install and Team ID PWA5… before installer (launchDaemonManagement.zsh:1126); all curl calls use --fail and timeouts; CI actions SHA-pinned with permissions: read-all; API credentials reach curl through --config - on stdin (Resources/Jamf-getDDMstatusFromCSV.zsh:655).
  • Info: Resources/JamfEA-DDM-OS-Reminder-User-Clicks.zsh:17 and Resources/monitorRemoteSession.zsh:111 append /usr/local:/usr/local/bin to root’s PATH; 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/tmp directory — 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:4693 and the case ${returncode} block — console user via launchctl 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 in ps and 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 on die() 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/latest and the github.com pkg 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; no https enforcement on typed URLs (S3) — Resources/Jamf-getDDMstatusFromCSV.zsh:655

Persistence

  • LaunchDaemon /Library/LaunchDaemons/<rdnn>.dor.plist, StartInterval 60, 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.log wording and the layout of SoftwareUpdateDDMStatePersistence.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>.dorm configuration 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 .old files are never pruned — reminderDialog.zsh:5269, reminderDialog.zsh:5014.
  • An Apple wording change in install.log makes the resolver return missing, 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 installer exit 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 -n syntax checks, Semgrep, Gitleaks, and ShellCheck in CI; demo mode and Resources/reminderDialogPreferenceTest.zsh for manual checks. No automated behavior or resolver fixture tests in the repo.
  • Rollback: Clean. Parameter 4 Uninstall removes 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

  1. 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).
  2. Atomically install dor.zsh and dor-starter.zsh after zsh -n (launchDaemonManagement.zsh:603), then the LaunchDaemon, bootstrap, and kickstart -k (launchDaemonManagement.zsh:887).
  3. Starter every 60 s: exit if dor.pid is live or NextScheduledReminder is in the future or FALSE; otherwise launch dor.zsh with DOR_LAUNCH_SOURCE=starter (launchDaemonManagement.zsh:825).
  4. Runtime globals: uptime, disk space via JXA with diskutil fallback (reminderDialog.zsh:190); log pre-flight and 10 MB rotation (reminderDialog.zsh:4999); root check, PID file, traps (reminderDialog.zsh:5036).
  5. Wait up to 120 s for a real console user, else fatal (reminderDialog.zsh:5052).
  6. Load preferences Managed → Local → Defaults with type validation (reminderDialog.zsh:1960), validate cadence values (reminderDialog.zsh:2094), resolve language (reminderDialog.zsh:2584).
  7. demo branch (source copy only; stripped from assembled artifacts) simulates a deadline and displays (reminderDialog.zsh:5106).
  8. Resolve DDM declaration from the last 4,000 install.log lines, corroborated against the persistence plist; on missing/conflict/noMatch/invalidVersion try the MDM fallback plist; unknown states fail closed (reminderDialog.zsh:4060).
  9. Evaluate past-deadline Prompt/Force and aggressive mode, with kill-switch file (reminderDialog.zsh:4542, reminderDialog.zsh:4582).
  10. 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).
  11. Build text, download icons, display swiftDialog; threshold monitor can refresh an open dialog (reminderDialog.zsh:4708).
  12. Handle return codes: Force mode loops until restart; otherwise schedule quiet-period or aggressive redisplay and open Software Update (reminderDialog.zsh:4808).
  13. quitScript writes the next schedule and cleans the runtime directory and PID file (reminderDialog.zsh:4969); fatal exits through the EXIT trap only (reminderDialog.zsh:1538).

Footguns

  • Fatal exits don’t schedule — reminderDialog.zsh:423, reminderDialog.zsh:1538 — fatal runs exit 1, and the EXIT trap only cleans up, so NextScheduledReminder stays 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 just dorm (. Log rotation at reminderDialog.zsh:5014 drops all history, so the quiet period and the 28-day periodic window reset.
  • dscl home 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 — installer status ignored; dialogCheck re-checks only -x (launchDaemonManagement.zsh:1174), so an older swiftDialog passes.
  • Demo Force restarts for real — reminderDialog.zsh:4682 — zsh reminderDialog.zsh demo Force reaches executeRestartAction "Restart" and runs /sbin/shutdown -r now on the admin’s test Mac when the timer expires. Not deployed (assembled artifacts strip demo mode).

Edge cases not handled

  • Apple changes install.log wording → resolver returns missing, 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 &quot;[FATAL ERROR]     ${1}&quot;; exit 1; }
# after
function fatal() {
    updateScriptLog &quot;[FATAL ERROR]     ${1}&quot;
    shouldManageDaemonScheduling &amp;&amp; scheduleNextReminderInMinutes 15 &quot;Retry after fatal error&quot; &gt;/dev/null 2&gt;&amp;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}]} )) \
            &amp;&amp; ! { [[ &quot;${ddmStatePersistenceStatus}&quot; == &quot;available&quot; ]] &amp;&amp; ddmDeclarationIsCorroborated &quot;${parsedDDMVersionString}&quot; &quot;${parsedDDMEnforcedInstallDate}&quot;; }; 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=&quot;${data//\&quot;/\&quot;\&quot;}&quot;
    printf &quot;%s&quot; &quot;${data}&quot;
# after
    data=&quot;${data//\&quot;/\&quot;\&quot;}&quot;
    [[ &quot;${data}&quot; == [=+\-@$&#039;\t&#039;]* ]] &amp;&amp; data=&quot;&#039;${data}&quot;
    printf &quot;%s&quot; &quot;${data}&quot;

Closes S2.

R4 — Robust home-directory lookup · Quick win

reminderDialog.zsh:5069

# before
loggedInUserHomeDirectory=$( dscl . read &quot;/Users/${loggedInUser}&quot; NFSHomeDirectory | awk -F &#039; &#039; &#039;{print $2}&#039; )
# after
loggedInUserHomeDirectory=$( dscacheutil -q user -a name &quot;${loggedInUser}&quot; | awk -F &#039;: &#039; &#039;/^dir:/ {print $2; exit}&#039; )

Handles spaces in home paths.

R5 — Verify swiftDialog after install · Quick win

launchDaemonManagement.zsh:1131

# before
        installer -pkg &quot;$tempDirectory/Dialog.pkg&quot; -target /
# after
        if ! installer -pkg &quot;$tempDirectory/Dialog.pkg&quot; -target /; then
            rm -Rf &quot;$tempDirectory&quot;
            fatal &quot;swiftDialog installer failed&quot;
        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-owned dor-state.plist and 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, and resolvePaddedEnforcementDateForCandidate.
  • 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.zsh and Resources/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.zsh and JamfEA-Pending_OS_Update_Version.zsh pattern-scanned (structure matches the runtime resolver).
  • Other files: Resources/monitorRemoteSession.zsh, Resources/localizationFilter.zsh, Resources/checkUserFocusDisplayAssertions.zsh scanned or read; .github/workflows/security-scan.yml scanned 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 -a is commented out (logging reaches the file through the LaunchDaemon’s StandardOutPath) 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.log forgery 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

Generated by Monocle

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).
  • allowParameterSecrets stays "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 — helpmessage concatenates ${ssid}, ${computerName} and ${loggedInUserFullname} raw into a JSON string literal; the script’s own jsonEscape() and jq --arg helpers are not used here.
  • Evidence: "helpmessage" : "'"${helpmessage}"'", followed by if ! 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, including Silent. A crafted SSID within the 32-byte limit can instead close the string and append keys such as infobuttonaction; jq keeps 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 jq before 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.log DDM resolver), Mac-Health-Check.zsh:8069 and Mac-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.plist with an owner check at Mac-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 to warning, 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 of spctl output while spctl’s accept or reject result and exit status are discarded; the URL check accepts any https://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 spctl rejects still installs as root whenever its origin= 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; spctl output for a forged certificate was not reproduced.
  • Fix: Require spctl -a -t install to exit 0 and check pkgutil --check-signature for Developer ID Installer: … (PWA5E9TQ59); pin the URL prefix to https://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() and buildInspectComputerInformationItemsJSON() (Mac-Health-Check.zsh:5726–5758)
  • Origin: Code — the config and compliance plist are deliberately root:wheel 0644 in /Library/Application Support/org.churchofjesuschrist/Inspect so swiftDialog can read them as the console user; the same release restricts the client log to root:admin 640 for 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:wheel and 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 unless allowParameterSecrets="true", loads them from a root-only 600 plist with owner, mode and symlink checks (Mac-Health-Check.zsh:290–297), exits Silent + production runs early when the only token was rejected, logs a warning, and hands secrets to curl on 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 argv for 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 1 then 0, 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.zsh and launchctl bootstrap system — Mac-Health-Check.zsh:4375 — root
  • jamf policy -event <trigger> (external checks, each run as root by Jamf) and jamf recon — Mac-Health-Check.zsh:10157, updateComputerInventory() — root
  • chown "${loggedInUser}" of the per-user Inspect directory (inside a root-owned 0755 parent, 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.bash writes AppleLocale to /Library/Preferences/.GlobalPreferences.plist and root’s preferences, restored by an EXIT trap — 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 a curl --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 as url = … 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) and github.com (swiftDialog pkg) — GET — default TLS — getLatestSwiftDialogPkgURL(), Mac-Health-Check.zsh:10394
  • sofafeed.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 (nc or curl), Apple networkQuality, clock-skew comparison — checkNetworkHosts(), Mac-Health-Check.zsh:10317, checkClockSkew() (pattern-scanned)
  • Jamf Pro (policy triggers and inventory) — through the jamf binary

Persistence

  • LaunchDaemon org.churchofjesuschrist.MHC — /Library/LaunchDaemons/org.churchofjesuschrist.MHC.plist runs /Library/Management/org.churchofjesuschrist/MHC.zsh as 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 networkQuality caches, 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/jq on macOS 15+, otherwise a root-owned install; a user-owned Homebrew jq is rejected.
  • Jamf Pro policies with custom triggers for external checks (symvBeyondTrustPMfM, symvCiscoUmbrella, symvCrowdStrikeFalcon, symvGlobalProtect) — enabled by default on Jamf Pro; a missing policy reports Error on 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.plist layout and install.log wording; Apple can change either in any release.
  • Organization values hardcoded in source: org.churchofjesuschrist domain, 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-owned jq, the script exits with a misleading “mainDialogJSON is invalid JSON” before its own clear jq error, and nothing reaches the log — Mac-Health-Check.zsh:1676 vs Mac-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.log fallback (S2).

Change & rollback

  • Testing: CI runs Semgrep, Gitleaks, zsh -n and 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 from Resources/Makefile whose postinstall runs 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

  1. Harden PATH, resolve a root-owned jq, parse Parameters 4–11, compute secret-free flags, then enable set -x for Debug — Mac-Health-Check.zsh:35–184.
  2. Create the per-run mktemp -d directory with an EXIT trap; load the secrets plist and reject parameter secrets — Mac-Health-Check.zsh:242–343.
  3. Normalize the Splunk mode (unknown → test); Silent + production with only a rejected Parameter 8 exits 1 — Mac-Health-Check.zsh:429–478.
  4. Cache gate (Silent + production): a root-owned force-fresh trigger or Parameter 11 forces a full run; otherwise a matching client scriptVersion plus a valid root-owned cached report sets clientSideSkipChecks — Mac-Health-Check.zsh:481–553.
  5. Discovery: MDM vendor from profiles (Jamf requires jamf.log, else exit 1), hardware, user, SSID, boot policies, VPN — Mac-Health-Check.zsh:768–1314.
  6. Create the dialog command and JSON files (600 plus a read ACL), build mainDialogJSON and the per-MDM list items, exit 1 if any fails validation — Mac-Health-Check.zsh:1352–1680.
  7. 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.
  8. Outside Silent: install or update swiftDialog and quit other swiftDialog windows — Mac-Health-Check.zsh:7206–7219.
  9. Build combinedJSON per 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.
  10. Launch swiftDialog through the Dock-named copy, with fallback to dialogcli — Mac-Health-Check.zsh:11026–11093.
  11. Run the checks through runConfiguredHealthCheck; Test mode marks every item success without running checks — Mac-Health-Check.zsh:11101–11483.
  12. 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 jq reported as invalid JSON — Mac-Health-Check.zsh:1676 — with no root-owned jq, validateJson fails because jq is not found, so the script prints “mainDialogJSON is invalid JSON” to stdout and exits before the explicit jq fatal at Mac-Health-Check.zsh:7009 and before anything is written to scriptLog.
  • 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 — when networkQuality fails, dl_throughput is empty, bc prints “Parse error: bad expression” to stderr (observed in isolation), mbps is empty, and the list item is still status: success; the bad result file is reused for 4 hours.
  • Home-directory parse — Mac-Health-Check.zsh:985 — dscl prints a value containing a space on its own line, so awk '{print $2}' returns a blank line plus the last word (isolated check: dscl . read /Users/root RealName gives [] 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.log also 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, missing jq) → 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 running jamf (Mac-Health-Check.zsh:2418–2426); a CrowdStrike check killed mid-run may leave the system AppleLocale at en_US if its EXIT trap never runs (inferred; signal delivery to Jamf’s child script not reproduced).
  • Mosyle plus a webhook → computerMdmURL stays empty, so the Slack button url is "", 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
    &quot;helpmessage&quot; : &quot;&#039;&quot;${helpmessage}&quot;&#039;&quot;,
# after (compute before mainDialogJSON)
helpmessageJSON=$( printf &#039;%s&#039; &quot;${helpmessage}&quot; | jq -Rs . )
…
    &quot;helpmessage&quot; : &#039;&quot;${helpmessageJSON}&quot;&#039;,

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 &quot;${jqBinary}&quot; ]] &amp;&amp; [[ &quot;${jqBinary}&quot; != &quot;/usr/bin/jq&quot; ]]; then
    function jq() { &quot;${jqBinary}&quot; &quot;$@&quot;; }
fi
# after
if [[ -z &quot;${jqBinary}&quot; ]]; then
    echo &quot;jq is required; install a root-owned jq (macOS 15+ ships /usr/bin/jq). Exiting.&quot;
    exit 1
elif [[ &quot;${jqBinary}&quot; != &quot;/usr/bin/jq&quot; ]]; then
    function jq() { &quot;${jqBinary}&quot; &quot;$@&quot;; }
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=&quot;${4:-&quot;Self Service&quot;}&quot;
# after
operationMode=&quot;${4:-&quot;Self Service&quot;}&quot;
case &quot;${operationMode}&quot; in
    &quot;Debug&quot; | &quot;Development&quot; | &quot;Self Service&quot; | &quot;Silent&quot; | &quot;Test&quot; ) ;;
    * ) echo &quot;Unrecognized operationMode &#039;${operationMode}&#039; (Parameter 4); exiting.&quot;; 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 &quot;scale=2; ( $dlThroughput / 1000000 )&quot; | bc )
# after
    if [[ &quot;${dlThroughput}&quot; != &lt;-&gt; ]]; then
        rm -f &quot;${networkQualityTestFile}&quot;
        dialogUpdate &quot;listitem: index: ${1}, icon: SF=$(printf &quot;%02d&quot; $(($1+1))).circle.fill weight=bold colour=${statusColorError}, iconalpha: 1, status: error, statustext: Unable to measure&quot;
        warning &quot;${humanReadableCheckName}: networkQuality returned no throughput&quot;
        return
    fi
    mbps=$( echo &quot;scale=2; ( $dlThroughput / 1000000 )&quot; | 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 &quot;/Users/${loggedInUser}&quot; NFSHomeDirectory 2&gt;/dev/null | awk -F &#039; &#039; &#039;{print $2}&#039; )
# after
    loggedInUserHomeDirectory=$( dscl -plist . read &quot;/Users/${loggedInUser}&quot; NFSHomeDirectory 2&gt;/dev/null \
        | plutil -extract &#039;dsAttrTypeStandard:NFSHomeDirectory.0&#039; raw -o - - 2&gt;/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 &quot;${inspectFileToPrepare}&quot; 2&gt;/dev/null || ! chmod 644 &quot;${inspectFileToPrepare}&quot; 2&gt;/dev/null; then
# after
    if ! chown root:wheel &quot;${inspectFileToPrepare}&quot; 2&gt;/dev/null || ! chmod 640 &quot;${inspectFileToPrepare}&quot; 2&gt;/dev/null \
        || { [[ -n &quot;${loggedInUser}&quot; ]] &amp;&amp; ! chmod +a &quot;${loggedInUser} allow read&quot; &quot;${inspectFileToPrepare}&quot; 2&gt;/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.md and SECURITY.md (root-owned state, parameter-secret rejection, trusted self-copy, /usr/local/bin removal, wrapper mktemp -d, Electron content validation, Not Running treated as failure) were each confirmed in code. Earlier install.log-based or /var/tmp-based weaknesses now remain only as the fallbacks in S2.
  • Unattended schedule. The nightly job can run networkQuality and softwareupdate --list on 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.zsh is 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, postinstall and 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.0 CHANGELOG.md section and relevant README sections were read for claims.
  • Semgrep. No zsh parser; zero results is not evidence of safety. The 7 errors are p/ci parse failures on embedded shell in security-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 RealName piped to the script’s awk pattern; bc with an empty operand; jq -Rs ., plutil -extract … - and the zsh <-> pattern for refactor snippets.
  • Not reproduced. S1 injection and abort, S3 spctl output 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

Generated by Monocle

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 Preferences files), S3 (package signer checks ignored verification status), S4 (non-HTTPS FullUpdaterLocation accepted), S5 (root sqlite3 write to a keychain path from the user’s home), the roll-up Low (unpinned Semgrep in CI), and all six prior non-security issues (substring pkill, root ${TMPDIR} cleanups, home-path parsing, Teams backgrounds staged in /private/tmp, app removed before a failed install, launchctl calls 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.zsh lines 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 $5 operations allowlist, and $6 (--allow-all-operations) is set only on admin-only policies. The code enforces this in self-service mode (exit 10), so it is a gate, not a finding.
  • User-facing policies don’t use test or debug mode (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 at Microsoft-365-Reset.zsh:2947
  • Origin: Deployment — the default self-service mode fails closed without $5 (exit 10); test and debug are documented operator modes (README.md:125) that only log a WARNING.
  • 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 $4 to test or debug and leaves $5 blank, users see every operation, including remove_office and remove_defender. That would be a Low secure-default gap (Rule 14), and debug would also write a full set -x trace (Microsoft-365-Reset.zsh:111) into the Jamf policy log.
  • Fix: Optional: require the same $6 opt-in in test and debug by 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/Microsoft is 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:50 says “only remove_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_office always 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:152 row, 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.toml sets 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:

  • PATH pinned 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 f and both sqlite3 edits run as the console user (Microsoft-365-Reset.zsh:524, Microsoft-365-Reset.zsh:2218, Microsoft-365-Reset.zsh:2649); the Office365V2 rename runs as the console user (Microsoft-365-Reset.zsh:2589).
  • pkgutil --check-signature must exit 0 and report an Apple-issued distribution certificate (Microsoft-365-Reset.zsh:653–654); the swiftDialog spctl assessment must exit 0 before the Team ID is compared (Microsoft-365-Reset.zsh:946–948).
  • Custom MAU channels require https:// for both ManifestServer and FullUpdaterLocation (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-install codesign check 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 ID PWA5E9TQ59, and the version is re-read after an upgrade (Microsoft-365-Reset.zsh:955–988, Microsoft-365-Reset.zsh:1018).
  • cfprefsd is 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 -d and forwards "$@" (Resources/createSelfExtracting.zsh:41–48); CI pins every Action to a commit SHA, sets permissions: read-all, and pins Semgrep to 1.179.0 (.github/workflows/security-scan.yml:18, .github/workflows/security-scan.yml:61).

Privilege map

  • Root rm -rf of system paths — Microsoft-365-Reset.zsh:396 — root
  • rm -rf of 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 --forget of 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 bootout and bootstrap of 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 cfprefsd for the console user and root — Microsoft-365-Reset.zsh:2658–2659 — root
  • Keychain deletions, sqlite3 keychain-database edits, Teams background and Office365V2 moves, tccutil reset, defaults writes, 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 via launchctl 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 — nscurl HEAD 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 with ChannelName=Custom) — nscurl GET — HTTPS required for the server and for FullUpdaterLocation — Microsoft-365-Reset.zsh:815–828
  • api.github.com/repos/swiftDialog/swiftDialog/releases/latest and the github.com release asset — curl --fail GET 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.com and swiftdialog.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 exits 20.
  • 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 exit 10 before any dialog appears (inferred).
  • Microsoft go.microsoft.com fwlink IDs and the Microsoft CDN — a retired link ID breaks repair for that app, reported as an operation failure (exit 20).
  • Hard-coded minimum versions (Office 16.73, OneDrive 23154.0, Teams 23247.0, MAU 4.49) — need periodic review; scripts/mofa-consult.zsh checks 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 $5 allowlist 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 as WARNING Failed to remove path lines and exit 20 — 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_outlook deletes the Zoom and Webex Outlook plugin support folders, which the README’s reset_outlook row 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, and reset_credentials delete every generic keychain item labelled exactly Exchange, 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_office preinstall hides skipped work. When the preinstall phase leaves an app bundle behind, the run exits 20 before any other selected operation runs (for example, remove_defender), and the summary lists only remove_office as 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, and test mode 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 manual sudo; 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

  1. Pin PATH, read $4–$6 defaults, then parse CLI flags; leading positionals are skipped until the first flag, and later bare positionals exit 10 — Microsoft-365-Reset.zsh:28, Microsoft-365-Reset.zsh:55–104.
  2. Normalize the mode (debug turns on set -x; an unknown mode exits 10); allow-all accepts only true/yes/1 — Microsoft-365-Reset.zsh:106–132.
  3. Create a root mktemp -d work directory and a root-owned 0644 command file; trap cleanup EXIT logs the summary and removes both — Microsoft-365-Reset.zsh:136–141, Microsoft-365-Reset.zsh:278–289.
  4. preflightChecks: require root and a real console user; resolve the home through dscl, then dscacheutil, then /Users/<name>; reject / and /var/root; accept the per-user temp folder only if trusted; enforce the self-service allowlist (exit 10); interactive modes run dialogCheck — Microsoft-365-Reset.zsh:2906–2954.
  5. Intro dialog (interactive only); cancel exits 0 — Microsoft-365-Reset.zsh:1035–1056, Microsoft-365-Reset.zsh:2971–2977.
  6. 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.
  7. 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.
  8. Destructive confirmation for remove_office, remove_outlook_data, remove_onenote_data, and remove_defender in interactive modes; an unacknowledged checkbox exits 2 — Microsoft-365-Reset.zsh:1243–1279.
  9. Start the progress dialog in the background, reading the command file — Microsoft-365-Reset.zsh:1281–1311.
  10. If remove_office is selected, run its preinstall teardown first; if app bundles remain, show the completion dialog and exit 20 — Microsoft-365-Reset.zsh:3006–3030.
  11. Dispatch each operation with removalFailures reset per operation; any path safeRemove couldn’t remove fails that operation — Microsoft-365-Reset.zsh:3032–3055, Microsoft-365-Reset.zsh:375–407.
  12. App resets call maybeRepairOfficeApp → repairFromMicrosoftPkg (download to a root-private directory, verify, move the original aside, install, codesign, restore on failure) and return 2 to defer cleanup after a repair — Microsoft-365-Reset.zsh:834–910, Microsoft-365-Reset.zsh:677–794.
  13. Finish the progress dialog, show completion (a warning icon on failure), offer a restart in interactive modes, then exit 20 if any operation failed, otherwise 0 — 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/ZoomOutlookPlugin on every reset with no repair, although remove_zoomplugin and remove_webexpt exist for that. Inherited verbatim from MOFA and the package-era Office_Outlook_Reset postinstall (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 exactly Exchange, 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_office preinstall leaves a bundle (for example, one blocked by TCC or a root-owned leftover) → exit 20 before the other resolved operations run, and the summary and completion dialog name only remove_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 &quot;/Library/Application Support/Microsoft/WebExPlugin&quot;
    safeRemove &quot;/Library/Application Support/Microsoft/ZoomOutlookPlugin&quot;
    safeRemove &quot;/Users/Shared/ZoomOutlookPlugin&quot;
# 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 &quot;remove_office preinstall phase failed&quot;
# after
        if ! removeOfficePreinstall; then
            appendFailure remove_office
            errorOut &quot;remove_office preinstall phase failed&quot;
            local skippedOp
            for skippedOp in &quot;${resolvedOperations[@]}&quot;; do
                [[ &quot;${skippedOp}&quot; == &quot;remove_office&quot; ]] &amp;&amp; continue
                appendFailure &quot;${skippedOp}&quot;
                warning &quot;Skipped ${skippedOp}: remove_office preinstall failed&quot;
            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 Exchange loop, log the matched item’s service and creator (never its account) on a test Mac, then add -s or -G to 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–13 and SECURITY.md:44, SECURITY.md:51–53 holds in code: user-context deletion without root fallback (Microsoft-365-Reset.zsh:392–394), reset operations leaving /Library/Managed Preferences alone (Microsoft-365-Reset.zsh:1786), the pkgutil exit and status check (Microsoft-365-Reset.zsh:653–654), https:// for FullUpdaterLocation (Microsoft-365-Reset.zsh:823–826), user-context keychain-database edits and Office365V2 rename (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: “only remove_office” removes managed preferences (README.md:50) also excludes remove_skypeforbusiness (S3).
  • Maintainer tooling. Every not-scored item from the prior review is closed. .deployMicrosoft365Reset.zsh runs commands without eval (line 148), writes release notes with mktemp -t (line 203), and stages with git add -u (line 399). scripts/mofa-consult.zsh defaults 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.zsh was read in full (lines 1–3068) and Resources/createSelfExtracting.zsh in 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 since 93629a5 plus a pattern scan.
    • .github/workflows/security-scan.yml (466 lines): lines 1–35 and 160–200, the diff since 93629a5, 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 since 93629a5), .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, and SECURITY.md.
  • Local-only, gitignored files. .deployMicrosoft365Reset.zsh, .codex/config.toml, .envrc, VERSION.txt, .DS_Store files, and Resources/Microsoft_Office_Reset_2.0.0b1.pkg with 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 (../MOFA at f78517a28a) 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 -x matched a 17-character process name (accessoryupdaterd), so exact-name force-quits of Microsoft PowerPoint, Microsoft Outlook, and Microsoft OneNote are not truncated.
    • /bin/rm -rf on 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 (ref 93629a5) 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

Generated by Monocle

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).

Posted in AI, macOS, Scripts, SecOps, Tips & Tricks

Related Posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.