Where Automation & Scripting Fails and How Bash Helps
A bounded macOS Bash automation workflow with evidence-based diagnosis, guardrails, rollback and validation, replacing unaccountable ad hoc scripts.

This playbook covers
Table of Contents
Table of contents
#Current Method
Many macOS
This pattern is common because Bash is already present on every Mac, requires no additional tooling, and is fast to write for a single machine. The operating context assumption that is often left invisible is that the script is being run interactively by a trusted administrator on one machine at a time; when the same script is later pushed at scale through Jamf Pro
#Improved Workflow
The improved workflow treats the Bash script as a bounded, idempotent unit of automation rather than a one-off command. It has four properties: a documented scope (exactly what the script changes and does not change), explicit precondition checks that cause the script to exit cleanly rather than proceed on missing dependencies, structured logging to a predictable location for later evidence collection, and an explicit rollback or reversal path for every state-changing action it takes.
Concretely, this means separating diagnosis from action inside the script: a read-only check phase that gathers evidence (OS version, disk space, process state, existing configuration value) runs first and can be executed independently; the state-changing phase only proceeds if the check phase passes, and it writes a timestamped log entry recording the before-state so that a human or a companion script can reverse the change later. This separation is directly useful whether the script is run interactively once, via a launchd job on a schedule, or pushed through an MDM script payload to many machines.
#Implementation
The example below diagnoses and adjusts a single bounded operational task: verifying and, if necessary, correcting a Spotlight indexing exclusion on a specified path, which is a common recurring task in environments with large build directories or network shares that should not be indexed. The workflow is chosen because it is state-inspectable via mdutil, has a documented rollback (removing the exclusion restores default indexing), and does not require destructive filesystem operations.
Roles and permissions: the check phase can run as the standard logged-in user for read access to indexing status; the state-changing phase requires administrator privileges because mdutil exclusion changes are scoped per-volume and require elevated rights outside of the user’s own home directory. Trade-off: running the whole script with elevated privileges simplifies deployment through MDM but widens the blast radius if the script has a logic error, so the recommended pattern below still separates the read-only check from the state-changing action even when both run as root via MDM.
#Code Reference
The script below performs the check phase only; it is read-only and safe to run in any environment, including production, without prior change control.
#Guardrails
- Never run the state-changing phase against a path derived from unvalidated user input; hardcode or explicitly validate the target path against an allow-list before use.
- Require the check phase to pass (exit code 0) before the state-changing phase executes; do not chain phases with
&&across a network deployment tool without capturing the check phase’s own log first. - Log every state-changing action with a timestamp and the pre-change value, stored outside of the path being modified, so rollback does not depend on data that the script itself is changing.
- Restrict who can edit or deploy the script to administrators with change-control access; treat the script itself as a configuration artefact under version control, not a disposable one-off.
#Validation
Validation confirms both that the intended state was reached and that the script did not silently fail on a subset of machines. After deployment, re-run the check phase independently (not the full script) against a sample of target machines and confirm the reported state matches intent. Cross-reference the structured log written during the state-changing phase against the count of machines targeted by the deployment tool; a mismatch indicates partial failure that needs investigation before the workflow is considered complete.

#Common Mistakes
- Treating a non-zero exit code from a called utility as informational rather than a stop condition, allowing the script to continue past a failed precondition.
- Hardcoding a username or home directory path instead of resolving it at runtime, which causes the script to silently do nothing (or fail loudly) on machines with a different console user.
- Deploying the combined check-and-change script through MDM without first validating the check phase alone across a representative pilot group.
- Omitting a pre-change log entry, which removes the only reliable basis for a rollback decision once the script has run across the fleet.
#Recovery
If the state-changing phase has run and produced an undesired result, recovery depends entirely on the pre-change log entry captured during execution. For the Spotlight exclusion example, recovery means removing the added exclusion path from mdutil‘s configuration and re-triggering an index of the affected volume, then confirming via the check phase that indexing status has returned to the pre-change baseline. If the log entry is missing or ambiguous, escalate to manual per-machine inspection rather than guessing at the prior state; do not run a broad corrective script against a fleet without first confirming the pre-change baseline on at least one representative machine.
#Measurable Outcome
Observable success is defined as: 100% of targeted machines show a check-phase pass on first re-validation, or a fully accounted-for exception list explaining each failure; the structured log for every run includes a resolvable pre-change value; and no state-changing phase runs without a corresponding successful check-phase log entry immediately preceding it in the same execution. These are inspectable directly from the log output and the mdutil status output, not inferred.
#Bash Automation Review Cadence and Escalation
Review the script’s guardrails and logging format whenever the target macOS version changes materially, since utilities such as mdutil can change flag behaviour between major releases; confirm current behaviour against Apple’s own documentation before relying on a flag’s meaning across an OS upgrade. Escalate to manual review, rather than re-running the automation blind, whenever the check-phase failure rate on a deployment exceeds a small tolerance the team has agreed in advance, since a high failure rate usually indicates an environmental assumption (such as console user resolution) that no longer holds.
#Checklist
- Check phase is read-only and independently runnable.
- State-changing phase only executes after a passing check-phase result in the same run.
- Pre-change value is logged outside the modified path before any change is applied.
- Rollback procedure is documented and has been exercised on at least one test machine.
- Validation sample has been re-checked independently after fleet deployment.
- Review cadence is scheduled against macOS version changes and utility behaviour changes.
#Prerequisites and Access Requirements
Before deploying the check phase through Jamf Pro or another MDM script payload, confirm the target Macs are enrolled with a supervised profile that permits script execution outside interactive login sessions; unsupervised machines may prompt for consent or silently skip the payload, producing a false negative in the deployment tool’s own success reporting. Confirm the administrator account used to author and upload the script holds a role with script-management rights in the MDM console, distinct from device-management rights, as some organisations separate these permissions deliberately. For the state-changing phase, confirm the deployment policy is scoped to run as root (the default for most MDM script payloads) rather than as the console user, since mdutil exclusion changes against volumes outside the user’s home directory require elevated rights as already noted; running as the console user will cause the state-changing phase to fail its own precondition check rather than partially apply.

#Change-Control Record Requirements
Every deployment of the state-changing phase, however small the pilot group, should be logged as a change record containing the script version (matched to its entry in version control), the target scope (smart group name or serial list), the date and time of deployment, the administrator who approved deployment, and the sample size used for post-deployment validation. This record is separate from the script’s own runtime log and exists to answer audit questions about who authorised a fleet-wide change, not what the change did technically.
#Expected Evidence on a Successful Run
A successful check-phase execution against a compliant machine should write a log line recording the volume identifier, the current exclusion list state as reported by mdutil -i-equivalent status output, and an explicit pass result with exit code 0. A successful state-changing run additionally records the pre-change exclusion list verbatim before appending the new path, followed by a second read of the exclusion list confirming the addition took effect. Absence of the second read-back in the log is itself evidence of a failure worth investigating, even if the script’s own exit code reported success, since it indicates the verification step did not execute as intended.
#Monitoring the Deployment
Where the deployment tool supports it, configure the script payload to return its final exit code to the console’s policy log so that failures surface in the standard reporting view rather than requiring log retrieval from every machine. Set up a recurring extension attribute or inventory collection that reads back the current exclusion state on a schedule (for example, daily) independent of the deployment event itself, so that configuration drift after the initial change—caused by an OS upgrade resetting indexing settings, for instance—is detected without waiting for the next scheduled script run.
#Realistic Failure Symptoms
A machine that has silently failed the state-changing phase typically shows no error in the MDM console’s policy summary but returns a check-phase failure on the next scheduled validation; this discrepancy is the most common indicator that the script exited early on a precondition without the deployment tool recognising it as a failure. Another realistic symptom is a machine reporting a passing check phase immediately after deployment but reverting to a failing state within a few days, which usually indicates an OS update or a competing configuration profile overwrote the exclusion outside of the script’s control. A third symptom is a cluster of failures confined to machines on a specific hardware model or macOS point release, which points to a utility behaviour difference rather than a scripting fault.
#Escalation Thresholds
Agree a numeric escalation threshold before the first fleet-wide deployment; a reasonable starting point is escalating to manual review when check-phase failures exceed five per cent of the targeted scope on first re-validation, or when any single failure cluster shares an identifiable common factor such as model or OS build. Below that threshold, individual failures can be queued for standard per-machine remediation without halting the wider rollout.
#Safe Rollback Actions
If a fleet-wide deployment must be halted mid-rollout, disable the MDM policy immediately to prevent further machines receiving the state-changing phase, then run the check phase alone against the already-targeted scope to establish how many machines were actually affected before rollback begins. Reverse the change only on machines confirmed via log evidence to have received it, using the recorded pre-change value, rather than applying a blanket reversal script against the entire scope, which risks altering machines that were never touched by the original deployment.
Related articles
Automation & Scripting
A Safer Automation & Scripting Operating Model for Bash
Design a bounded, least-privilege Bash automation workflow for macOS with launchd scheduling, validation steps, guardrails and a tested rollback path.
Automation & Scripting
Making Automation & Scripting Repeatable with Bash
Design, validate and safely recover a bounded macOS Bash automation workflow with idempotency, guardrails and rollback steps.
DevOps & Automation
Making DevOps & Automation Easier to Recover with GitHub Actions
Design a bounded GitHub Actions workflow with explicit validation and rollback steps to ensure safe recovery of automated tasks.
DevOps & Automation
Operating DevOps & Automation Reliably with GitHub Actions
Architecture, validation and rollback for a bounded GitHub Actions deployment workflow, covering environment protection, OIDC scoping, concurrency control and tested recovery paths.
Discover more
Learn More About KBY
About KBY
Learn about our mission, editorial standards, and commitment to trusted engineering knowledge.
Why Trust KBY
Explore the processes and policies that ensure our publications are accurate, useful, and responsible.
Newsletter
Get our latest editorial publications, research and practical insights sent directly to your inbox.
Was this useful?
Operate smarter, with fewer recurring tickets.
Receive new operational playbooks, incident-prevention guidance, automation scripts and recovery runbooks.
Comments
Add a thoughtful note on Where Automation & Scripting Fails and How Bash Helps. Comments are checked for spam and held for moderation before appearing.