Software · macOS troubleshooting

macOS updates, permissions, storage and the Mac-specific quirks

library 52 tutorials · 5 categories ≈ 4 h read (full library) Intel & Apple Silicon

macOS problems cluster into five families, and each family has a different first question: an update that fails asks which stage broke; a permission denial asks which of the two protection layers is answering; a full disk asks what category is holding the space; a tool that works in one context but not another asks what differs between those environments; and a vague "the Mac is misbehaving" asks whose problem this actually is. This library is the collection of those question paths — fifty-two distinct tutorials, each built around one observable problem, with the steps, commands, decision points and verification that fix it rather than guess at it.

Every tutorial follows the same shape on purpose: problem and symptoms (what you see, and how it differs from look-alike failures), likely causes and diagnosis (the plausible causes and the checks that separate them), a step-by-step solution with screenshots marked for each step where they will help, and verification and next steps (how to know it worked, and where to go when it did not). Diagnosis is always separated from remediation: no tutorial tells you to delete a file or change a setting before establishing that the change addresses your actual problem. Where behavior varies by macOS version, hardware architecture, filesystem or application, the tutorial says so instead of assuming.

Two conventions run through the whole library. Commands are presented read-only first: inspection commands come before modification commands, and anything that changes the system is labeled as doing so, with what it affects stated up front. And every procedural step carries a marked image slot — hidden until a screenshot is actually added — so the page stays clean now and gains visuals later without restructuring any article.

§01

Orientation

How to use this library

Start from the symptom, not from a category name. The index below searches titles, summaries and symptom keywords across all 52 tutorials; the table below maps common starting points to their categories. If two tutorials seem close, open the one whose first section describes your screen more precisely — each targets one distinct state of a failure, and that distinction is what makes the steps apply.

Starting points: map your symptom to the category that covers it
Your situationStart in
An update will not download, install or finish — or the Mac loops back into the updaterCategory A (tutorials 01–10)
An app cannot read a folder, Terminal says "Operation not permitted", permission prompts keep repeatingCategory B (tutorials 11–22)
The disk is full or nearly full, System Data looks enormous, deleted files did not free spaceCategory C (tutorials 23–32)
A tool fails with "bad CPU type", works in Terminal but not from an app, Gatekeeper blocks a downloadCategory D (tutorials 33–44)
You need to read logs or crash reports, find what is eating resources, or document the problem for supportCategory E (tutorials 45–52)

Reading the steps Each numbered step is one action with its own marked image slot. Steps that run commands say whether the command reads state or changes it; destructive steps name what they affect and how to inspect the target first. If a step's result differs from what the tutorial expects, stop and follow the "next diagnostic" pointer rather than continuing — a mismatched observation is information, not an obstacle.

Tutorial index

Search by symptom, title or keyword, or filter by category and difficulty. The list below and the full tutorials further down this page stay in sync — filtering hides both.

Showing 52 of 52 tutorials

01A · Updates, upgrades & recoveryBeginner

Prepare a Mac for a major macOS upgrade before you install it

A major upgrade is the one macOS change where a bad outcome can cost you days of work. Ten minutes of preparation — verifying the backup actually works, confirming free space and supported version — turns an irreversible gamble into a routine operation.

02A · Updates, upgrades & recoveryBeginner

Fix a macOS update that is stuck downloading

A download percentage that does not move for hours is almost never a broken macOS installation — it is a delivery problem: the network, the disk, or a stalled update process. Work through those three in order and most stuck downloads resume within minutes.

03A · Updates, upgrades & recoveryIntermediate

Investigate an update that appears frozen during installation

During a major upgrade the installer rewrites the system volume, and several phases show no visible progress by design. The difference between “slow but working” and “genuinely stuck” is what the machine is doing underneath — and forcing power off at the wrong moment is the one move that can make a recoverable situation unrecoverable.

04A · Updates, upgrades & recoveryIntermediate

Diagnose repeated update failures and their error messages

When the same update fails identically on every attempt, the cause is stable and findable. The error text is not decoration — it names the stage where the pipeline broke, and working from that stage instead of retrying blindly is the difference between a fix and a loop.

05A · Updates, upgrades & recoveryAdvanced

Investigate a Mac that keeps returning to the Software Update screen

A machine that reboots straight back into the installer instead of your desktop is telling you the new system volume will not boot. The diagnostic question is why: a corrupted install is fixable from Recovery, but failing storage behind the same symptom is not — and only a disk check can tell them apart.

06A · Updates, upgrades & recoveryIntermediate

Fix an update that downloads completely but never installs

The download finished — that part is done. What remains is the installer's own gate: it will not start (or will not finish) when free space ran out after the download landed, a restart was required and missed, or the staged payload is corrupt. Each has a specific test.

07A · Updates, upgrades & recoveryBeginner

Tell an unsupported Mac from a compatible one with a fixable problem

When Software Update offers no new macOS at all, there are only two explanations: this model is not on the release's support list, or a compatible Mac is failing to see the catalog. Ten minutes of checking settles which one you have — and the answers lead to completely different next steps.

08A · Updates, upgrades & recoveryIntermediate

Investigate Wi-Fi, peripheral and app problems that appear after an update

A major upgrade changes the system frameworks underneath everything else, and some things need a conscious re-approval or a vendor's updated build before they work again. Sorting “the OS broke it” from “this app needs its own update” saves you from rolling back an operating system to fix one program.

09A · Updates, upgrades & recoveryIntermediate

Decide when a problem needs Safe Mode, Recovery, or ordinary troubleshooting

Safe Mode and Recovery are powerful, but each changes a different set of conditions — and using the wrong one wastes an hour while masking the real test. This tutorial is the decision aid: what each environment actually does, when it is the right tool, and how to enter each on both Apple Silicon and Intel Macs.

10A · Updates, upgrades & recoveryAdvanced

Plan a rollback or recovery strategy when an update goes seriously wrong

“Can I just go back?” is the first question after a bad upgrade — and macOS's honest answer is: sometimes, but only through specific routes with specific preconditions. This tutorial maps what rollback actually means on a Mac, which routes exist, what each one depends on, and how to plan the one that fits your situation.

11B · Privacy, permissions & accessBeginner

Understand macOS privacy permissions: what each one actually governs

macOS asks for permission in ways that look interchangeable — a dialog, a settings list, an “Allow” button — but each permission governs a different layer of access. Knowing which layer your problem lives in is what turns a random search through Privacy settings into a two-minute fix.

12B · Privacy, permissions & accessBeginner

Grant and verify Full Disk Access for a trusted application

Full Disk Access is the broadest routine privacy grant macOS offers, and the one most often needed by backup tools, developer utilities and system maintenance software. Granting it correctly — to the right binary, verified with a real test — takes five minutes; granting it carelessly is how machines accumulate access they no longer need.

13B · Privacy, permissions & accessBeginner

Fix an app that cannot access Desktop, Documents, Downloads or another folder

macOS treats your Desktop, Documents and Downloads as protected locations: apps must be granted access to them specifically, and a single denied prompt years ago can still be the reason an app cannot see files today. The fix is usually one toggle — if you can find the right entry for the right binary.

14B · Privacy, permissions & accessIntermediate

Troubleshoot Screen Recording permission problems and blank captures

A screen capture that comes out blank or black is almost always the Screen Recording permission missing, stale, or attached to the wrong binary — not a broken camera of pixels. The diagnosis is a short sequence: check the list, restart the app, and test with a capture you can judge at a glance.

15B · Privacy, permissions & accessIntermediate

Fix Accessibility permissions that block automation tools from controlling apps

Automation tools that simulate input or drive other applications fail in a characteristically silent way: they launch, look healthy, and simply do nothing. That silence is the Accessibility permission working as designed — and the diagnosis is to grant it deliberately, then prove it with a small action you can watch happen.

16B · Privacy, permissions & accessIntermediate

Investigate an application that keeps asking for the same permission

A permission prompt that returns for the same app, over and over, is not a glitch — it is macOS telling you the grant you gave no longer matches the binary asking. The most common reason is an update that changed the app's code signature; the fix is to recognize which entry is stale and replace it, not to keep clicking Allow.

17B · Privacy, permissions & accessIntermediate

Fix Terminal commands that receive permission-denied errors

Terminal denials come in two flavors with different fixes: a Unix permission error (the file's owner/permission bits say no) and a macOS privacy denial (the system shields that data from your terminal app regardless of who you are). Reading which one you have takes one line of output — and reaching for sudo on the wrong one wastes time while changing nothing.

18B · Privacy, permissions & accessIntermediate

Distinguish Unix file permissions from macOS privacy protections

macOS enforces access through two independent layers — the classic Unix permission model and the system's privacy framework — and a denial is only fixable in the layer that produced it. This tutorial builds the mental model: what each layer checks, how to tell which one refused you, and why fixes aimed at the wrong layer change nothing.

19B · Privacy, permissions & accessAdvanced

Investigate ownership and permission problems on external drives

External drives fail in permission ways that built-in disks never do, because their filesystem decides which of the two access layers even exist. On exFAT volumes there are no Unix permissions at all — so a “permission denied” there points somewhere else entirely; on APFS or HFS+ volumes there is ownership to inspect and, with care, to correct.

20B · Privacy, permissions & accessIntermediate

Fix permission failures in IDEs, scripts and AI coding tools

Developer tools fail on permissions in a pattern that confuses everyone who thinks of Terminal as the source of truth: an IDE launched from Finder is its own privacy subject, and the scripts it runs are judged by the IDE's grants — not your terminal's. Granting Full Disk Access to Terminal changes nothing for a GUI-launched editor, and this tutorial maps where each grant actually needs to live.

21B · Privacy, permissions & accessIntermediate

Investigate application access problems that appear after an update or reinstall

An app that worked yesterday and cannot access anything today, with the only change being an update or reinstall, is almost always a privacy-identity problem: the system no longer recognizes the new binary as the one that holds your grants. The fix is a deliberate re-grant — but knowing why it happened is what stops you from chasing ghosts in the wrong places.

22B · Privacy, permissions & accessBeginner

Audit your granted permissions and reduce unnecessary access safely

Privacy lists grow by accumulation: every prompt answered, every helper added, every app that earned a grant in 2023. An audit is how you convert that history into a deliberate set — removing what no longer exists, questioning what still does, and keeping the machine's access surface as small as your actual workflow needs.

23C · Storage, APFS & snapshotsIntermediate

Investigate unexpectedly large “System Data” on your startup disk

“System Data” is the storage category that swallows disks without explanation — and it is not one thing but a bucket: caches, logs, virtual memory files, local snapshots and miscellaneous system state. Investigating it means finding which member of the bucket grew, with read-only tools first, before anything is deleted.

24C · Storage, APFS & snapshotsBeginner

Locate large files and directories with Finder and Terminal

Before you can free space, you need to know where it is. Finder's search handles the visible half of your disk; Terminal's size commands handle the rest — including the Library directories Finder hides by default. This tutorial gives both workflows, starting from the safe places and explaining what each command does before you run it.

25C · Storage, APFS & snapshotsIntermediate

Understand why free-space numbers differ between reports

Your Mac can report three different “free space” numbers on the same day — Finder's Storage view, Terminal's disk reports, and the figure an app shows you — and none of them is wrong. Each measures a slightly different slice of an APFS container with shared space, purgeable data and snapshots. This tutorial explains what each number actually counts so the discrepancies stop looking like errors.

26C · Storage, APFS & snapshotsAdvanced

How APFS containers and shared volumes shape your available disk space

APFS does not give each volume its own fixed slice of disk — it gives them all one shared pool, the container. That single design decision explains a family of confusing observations: volumes that report identical free space, disks that fill up “from nowhere,” and capacity behavior that no partition-table mental model can predict. This tutorial builds the correct model.

27C · Storage, APFS & snapshotsIntermediate

Inspect local Time Machine snapshots and when they get reclaimed

Local snapshots are invisible backups macOS keeps on your internal disk — and they hold onto deleted files until the system decides to reclaim them. Inspecting them is a two-command job; understanding their lifecycle (when they appear, when they go) is what turns that inspection into an actual explanation for missing free space.

28C · Storage, APFS & snapshotsIntermediate

Fix storage that stays nearly full after deleting large files

Deleting 50 GB of files and watching available space stay put is not a bug — it is the disk telling you the data still has an owner. Local snapshots, unemptied trash, and cloud-offloaded files that re-download are the three usual owners, and each has a specific check.

29C · Storage, APFS & snapshotsIntermediate

Clean Xcode DerivedData, build artifacts and simulator data safely

A development Mac accumulates Xcode's working data the way a workshop accumulates shavings: individually harmless, collectively gigabytes. The good news is that nearly all of it is regenerable — build outputs, simulator state, intermediate caches — which makes this one of the safest cleanups on this page, provided you inspect sizes first and understand what each deletion costs in rebuild time.

30C · Storage, APFS & snapshotsIntermediate

Inspect Homebrew caches and package-manager artifacts before cleaning them

Package managers are designed to keep copies of what they download — caches exist so reinstalls and downgrades are fast. That design is exactly why a developer's disk quietly fills with bottles, tarballs and old versions nobody thinks about. The cleanup workflow here is inspect-then-prune: measure what each manager holds, use its own pruning tools first, and only touch raw cache directories when the tooling falls short.

31C · Storage, APFS & snapshotsAdvanced

Investigate runaway application logs, caches and temporary files

Some software does not just use disk — it writes to it continuously, and when a log or cache grows without bound the disk fills on a schedule you can watch. Finding the writer requires separating system logging (which macOS manages for you) from application-level files (which only you can fix), and handling each with the right tool.

32C · Storage, APFS & snapshotsBeginner

Plan a safe storage cleanup that preserves what matters

Disk cleanup fails in one of two ways: it does not free enough space, or it frees space by deleting something irreplaceable. A plan prevents both — by ordering work from least to most risky, verifying after every step, and treating personal data and system directories as categories with different rules entirely.

33D · Developer quirks & compatibilityIntermediate

Diagnose architecture mismatches between Apple Silicon and Intel in development tools

A binary built for the wrong CPU architecture fails in ways that look like random corruption: “bad CPU type in executable,” instant crashes, or tools that work on one Mac and not another. The diagnosis is a short set of commands that establish three facts — what your Mac actually is, what the binary actually is, and whether translation is silently involved.

34D · Developer quirks & compatibilityAdvanced

Investigate conflicting Homebrew installations and executable paths on Apple Silicon

Apple Silicon Macs can host two Homebrew installations at once — a native arm64 one in /opt/homebrew and an Intel (Rosetta) one in /usr/local — and when both exist, your shell may be running tools from the wrong one without saying so. The diagnosis is about PATH order and which prefix each tool actually resolves to; the fix is choosing one canonical installation and making everything agree.

35D · Developer quirks & compatibilityIntermediate

Diagnose a command that works in Terminal but fails from a GUI application

The same command succeeds in Terminal and fails from an app's run button, menu item, or scheduled task — not because the command changed, but because the environment that runs it did. GUI applications on macOS start with a minimal environment that your shell configuration never touches, and most of this failure class is PATH.

36D · Developer quirks & compatibilityAdvanced

Understand shell startup files: interactive shells, login shells, and GUI environments

Your configuration exists in several startup files, and which of them run depends on how the shell was started — a distinction that explains why a setting works in one terminal window and not another, and why GUI applications see none of it. This tutorial maps each context to the exact files it reads, so environment questions become lookups instead of guesswork.

37D · Developer quirks & compatibilityIntermediate

Diagnose case-sensitivity problems between macOS filesystems and other environments

macOS's default filesystem treats names that differ only in letter case as the same file — while Linux, CI servers and many build tools do not. The result is a classic failure family: code that works on your Mac and breaks on the server (or git conflicts that appear from nowhere). The diagnosis is a two-second test; the fixes range from renaming to understanding why reformatting is usually the wrong answer.

38D · Developer quirks & compatibilityAdvanced

Troubleshoot file-watcher limits and missed filesystem events in large projects

A dev server that stops noticing your edits is usually not broken — it is overwhelmed. File-watching on macOS has real limits: per-process file descriptor ceilings, event coalescing under load, and watchers that silently give up when a project's directory count grows past what they can track. The diagnosis separates “the tool gave up” from “the event never happened” — and the fixes are about scope, not magic.

39D · Developer quirks & compatibilityIntermediate

Investigate Gatekeeper, quarantine and apps that macOS refuses to open

“The application cannot be opened because the developer cannot be verified” is not a broken app — it is Gatekeeper doing its job on a file that arrived with a quarantine mark. Understanding where the mark comes from, what the legitimate bypass paths are, and why disabling Gatekeeper system-wide is the wrong fix turns this from a scuffle into a five-minute diagnosis.

40D · Developer quirks & compatibilityAdvanced

Diagnose code-signing and notarization errors in locally built applications

Locally built apps fail in signature-specific ways: the “app is damaged” error after a rename or modification, verification failures that name nested binaries, and Gatekeeper rejections that are really signing-configuration problems. The diagnosis follows one rule — a code signature covers the app's contents as a sealed whole — and the fixes range from ad-hoc signing for local work to proper notarization for anything that leaves the machine.

41D · Developer quirks & compatibilityBeginner

Fix executable-permission problems in shell scripts and development tools

“Permission denied” on a script you just wrote is almost always the missing executable bit — a one-command fix that becomes recurring trouble only when the bit is lost in transit (zips, some copy paths) or never committed (git does track it, but only if you set it). This tutorial covers the fix, the variants, and why the same error can mean two different things.

42D · Developer quirks & compatibilityIntermediate

Troubleshoot Xcode command-line tools, SDK selection and compiler configuration

Compiler failures on macOS often trace back to one place: the system's selection of which developer toolchain is active — the standalone command-line tools or a full Xcode installation, and which SDK each build uses. The selection is inspectable and switchable with standard commands, and most “compiler is broken” mysteries resolve once you see what the system actually has pointed where.

43D · Developer quirks & compatibilityIntermediate

Diagnose environment-variable and PATH differences across Terminal, IDEs and automation

The same machine can present three different environments: your Terminal's fully configured one, a GUI application's minimal launch environment, and an automation context's own reduced set. When a variable exists in one and not another, the bug is never “the variable vanished” — it is that two contexts were silently compared. This tutorial gives the comparison method and the fix patterns, ranked from explicit to hacky.

44D · Developer quirks & compatibilityAdvanced

Troubleshoot symlinks, mounted volumes and path assumptions that misbehave on macOS

macOS hides its real paths behind symlinks (/tmp is not where it says it is), mounts removable storage at a location that can disappear between sessions, and resolves path strings in ways that tools disagree about. Code written against the visible names — or against another operating system's layout — inherits all three surprises. This tutorial maps the actual layout and the assumptions to check.

45E · Diagnostics & workflowsIntermediate

Use Console and macOS logs to investigate an application failure

When an app crashes or misbehaves, macOS has already recorded the evidence: a crash report with the exception and faulting thread, plus the unified log stream around the moment of failure. Reading those records — instead of guessing from the symptom — turns “it quit” into a specific, reportable finding.

46E · Diagnostics & workflowsBeginner

Use Activity Monitor to find the process consuming your CPU, memory or disk

A slow Mac, a spinning fan, or a disk that never goes quiet all reduce to one question: which process is doing the work? Activity Monitor answers it directly — but only if you read the right columns and know that the memory number everyone looks at first is not the one that matters most.

47E · Diagnostics & workflowsIntermediate

Diagnose a Mac that behaves differently in Safe Mode

If a problem disappears in Safe Mode, you have just run the most informative test available on macOS: it tells you the cause is third-party software that Safe Mode excludes. The value is not staying in Safe Mode (it is a reduced-functionality test environment) but using the difference to eliminate suspects one by one back in normal operation.

48E · Diagnostics & workflowsAdvanced

Investigate startup and background-task problems: launch agents and daemons

Software that restarts itself after you quit it, login that takes forever, or a background process you cannot account for are all managed by the same system: launchd and its agent/daemon definitions. Learning to read those definitions — where they live, what they declare, and how to disable one cleanly — turns invisible background behavior into inspectable configuration.

49E · Diagnostics & workflowsIntermediate

Build a non-destructive diagnostic report for a support request

Support requests succeed or fail on the quality of their evidence. A diagnostic report assembled from read-only commands — system identity, disk state, top processes, recent crash reports, targeted log excerpts — gives the other side everything they need to start working instead of asking you to re-describe symptoms. Every command in this workflow changes nothing on your machine.

50E · Diagnostics & workflowsBeginner

Create a safe pre-update maintenance checklist for a developer's Mac

A developer's Mac carries more state than a general-purpose one: toolchains with version-sensitive components, uncommitted work, external drives with project data, and caches that double as capacity problems. A pre-update checklist converts the upgrade from an event you hope goes well into a procedure whose steps are verifiable before the first click — this is tutorial 01's preparation, specialized for development machines.

51E · Diagnostics & workflowsIntermediate

Distinguish a temporary macOS issue from an app bug or a persistent configuration problem

Every Mac problem falls into one of three ownership classes: a transient system state that clears on its own or with a reboot, an application defect that follows the app across conditions, or a persistent configuration that reproduces reliably in your specific setup. The class determines everything — what to test, what to report, and whether any fix is even yours to make. This tutorial is the decision procedure that assigns the class.

52E · Diagnostics & workflowsBeginner

Create a repeatable troubleshooting record that actually gets used

The difference between troubleshooting that compounds and troubleshooting that repeats is a record: a consistent document of what the symptom was, what changed recently, what was tested with what result, and what finally verified as fixed. This tutorial provides the template and the discipline around it — short enough to fill in under pressure, structured enough that six-months-later-you can read it without archaeology.

§03

Category A · 10 tutorials

Updates, upgrades & recovery

The lifecycle of a macOS update — preparation, download, installation, failure and the honest limits of going back. Each tutorial targets one observable state of the update process, so you can open the one that matches what your screen actually shows.

01

Prepare a Mac for a major macOS upgrade before you install it

A · Updates, upgrades & recoveryBeginnerIntel & Apple Silicon

A major upgrade is the one macOS change where a bad outcome can cost you days of work. Ten minutes of preparation — verifying the backup actually works, confirming free space and supported version — turns an irreversible gamble into a routine operation.

Problem and symptoms

This tutorial has no failure to diagnose: it runs before the problem exists. You see a banner in System Settings offering a new major macOS version (for example, moving from one numbered release to the next), and you want to install it without risking data loss, a broken development toolchain, or discovering afterwards that your backup is empty when you need it.

It helps to be precise about what you are doing. A point update (a new patch number within the same major release) changes relatively little and rarely needs preparation beyond a normal backup habit. A major upgrade replaces system frameworks, may change which applications remain compatible, and on many Macs re-prompts approval for third-party system extensions. Firmware-level components can also be updated as part of the process, and those parts are not something you can simply undo. That asymmetry — easy to apply, hard to reverse — is exactly why the preparation below exists.

Likely causes and diagnosis

The realistic failure modes after an unprepared upgrade fall into three families. First, data loss without a usable restore point: the machine becomes hard to use and the Time Machine backup you assumed existed turns out to be months old or missing entire volumes. Second, toolchain breakage: compilers, package managers and IDE plugins built against the previous system stop working, and without a record of what was installed you cannot rebuild the environment quickly. Third, insufficient storage: the upgrade download alone is several gigabytes, and the installer needs additional headroom to stage the new system volume; a disk that looked fine in June may not have room now.

Diagnosis at this stage is therefore a set of verifications rather than fixes. Each item below produces evidence you can act on: a backup timestamp you actually checked, a free-space number measured today, and a supported-version confirmation for your exact model. If any check fails, the upgrade waits — that is the decision the preparation is designed to make easy.

Step-by-step solution

  1. Confirm the target version supports this Mac. Open About This Mac (Apple menu) and note the model identifier shown under More Info, then compare it against Apple's supported-models list for the release you are considering. If your model is absent, stop here — no preparation will make an unsupported upgrade appear.

    About This Mac window showing the chip name and the model identifier line under More Info
    Step 1 — About This Mac with the model identifier visible, used to check Apple's supported-models list · img/m-01-s1.png
  2. Verify a Time Machine backup is recent and complete. Open System Settings → General → Time Machine (or the Time Machine menu bar item) and check the date of the last successful backup, then open the Time Machine browser once to confirm you can actually see your recent files. A backup that has never been browsed is an assumption, not a safety net.

    Time Machine settings showing the last successful backup date and the connected backup disk
    Step 2 — Time Machine status showing the last successful backup before starting the upgrade · img/m-01-s2.png
  3. Check free storage on the startup disk. In About This Mac → Storage, read the available space. The major-upgrade download is several gigabytes and the installer needs further room to stage the new system volume; if free space is tight, clear it first (see category C of this library) rather than hoping the installer copes.

    Storage settings bar showing used and available space on the startup disk
    Step 3 — Storage view with enough free space for the download plus staging headroom · img/m-01-s3.png
  4. Save, commit and push your work. Close open documents, commit any uncommitted code, and push branches to a remote repository. For developers, record the versions of your toolchain now (for example the output of your language runtimes and package managers) so you can diff against them after the upgrade and spot what actually changed.

    Terminal window showing version commands for node, python and a package manager before the upgrade
    Step 4 — Terminal recording current toolchain versions before the upgrade · img/m-01-s4.png
  5. Start the upgrade from System Settings → General → Software Update. Read the release notes once for anything that affects your workflow (changed permissions prompts, removed utilities), keep the Mac on power, and let the process run to completion without interrupting it.

    Software Update screen showing the available major macOS version and its release notes link
    Step 5 — Software Update offering the major upgrade, ready to install · img/m-01-s5.png

Verification and next steps

You are ready when three facts hold: a backup exists that you have actually browsed, free space comfortably exceeds the download size with staging headroom to spare, and your model appears on Apple's support list for the target release. After the upgrade completes, verify the outcome rather than assuming it — check the new version in About This Mac, re-run your toolchain version commands and compare them with the pre-upgrade record, and confirm that any third-party system extensions show an approval prompt you can consciously accept.

When this does not apply Point updates and security updates within the same major release are low-risk; a current backup habit is enough preparation for those. The full pre-update routine in section 08 of this page is written for exactly that lighter case.

02

Fix a macOS update that is stuck downloading

A · Updates, upgrades & recoveryBeginnerIntel & Apple Silicon

A download percentage that does not move for hours is almost never a broken macOS installation — it is a delivery problem: the network, the disk, or a stalled update process. Work through those three in order and most stuck downloads resume within minutes.

Problem and symptoms

The Software Update screen shows a downloading state — a percentage, a spinning indicator, or simply “Downloading” — and nothing changes for a long time. On a fast connection the major-upgrade download should make visible progress within minutes of starting; if the number has not moved at all, or creeps forward only to stop again, the transfer is effectively stalled.

Two look-alike states are worth separating before you do anything. A download stuck at a low percentage (single digits) is usually network-side. A download that crawls at high speed for a while and then freezes can be disk-side: the partial file no longer fits, or the update process has lost track of it. The fix path differs in one important respect — restarting the Mac resumes an interrupted download from where it left off, so a restart is safe to try early and costs little.

Likely causes and diagnosis

Three causes cover most cases. First, the network path: a failing Wi-Fi connection, a router that drops large sustained transfers, or a proxy/firewall that blocks Apple's update CDN. Second, insufficient free space: the download must fit on the startup disk before installation can even be considered, and a disk that was fine in the morning may not have room after other activity. Third, a stalled update process: the background software-update service occasionally wedges mid-transfer; this is the state a clean restart clears.

Diagnose by elimination, cheapest test first. Check free space (no tools needed), then test the network path with a different connection if one is available, and only then treat the update process itself as suspect. If you have Terminal access, the command below tells you whether Apple's catalog is reachable at all — a working list response proves the network path to Apple is healthy and points suspicion back at the local transfer state.

Terminal — check update reachability
# Lists the updates Apple's catalog currently offers this Mac.
# Read-only: it does not install or download anything.
softwareupdate -l

A list of available updates means the network path to Apple works. An error or an empty hang points at connectivity — fix that before touching the update process.

Step-by-step solution

  1. Record the exact state: the percentage, how long it has been unchanged, and whether the Mac is on battery or power. This baseline tells you later whether a fix moved the needle at all.

    Software Update screen showing a download percentage that has not changed
    Step 1 — The stuck download state, recorded with its percentage and how long it has been frozen · img/m-02-s1.png
  2. Check free space in About This Mac → Storage. If available space is below the size of the update plus a comfortable margin, stop here and free storage first (category C of this library) — no network fix will help a disk that cannot hold the file.

    Storage settings showing low available space on the startup disk
    Step 2 — Storage view confirming whether the download can physically fit · img/m-02-s2.png
  3. Test the network path. Restart your router, or better, move to a different connection (Ethernet, or a phone's personal hotspot) and watch whether the percentage starts moving again. A transfer that resumes on another network was never a macOS problem.

    Mac connected to a phone personal hotspot while the update download resumes
    Step 3 — Update download resuming after switching to a different network · img/m-02-s3.png
  4. If the percentage still will not move, restart the Mac. macOS resumes an interrupted update download from where it stopped, so a reboot is a safe, low-cost way to clear a wedged update process.

    Mac restarting and returning to the Software Update screen with the download resumed at its previous percentage
    Step 4 — After restart, the download resumes from its previous percentage · img/m-02-s4.png
  5. If a restart did not help, open Terminal and run softwareupdate -l. If it lists updates, the network path is healthy and the local transfer state is the problem; if it errors or hangs, treat this as a connectivity issue (proxy settings, firewall rules, ISP-side blocking of Apple's CDN).

    Terminal showing softwareupdate -l returning a list of available updates
    Step 5 — Terminal output of softwareupdate -l confirming Apple's catalog is reachable · img/m-02-s5.png
  6. Last resort, advanced: the partial download lives in a system cache folder (/Library/Updates). Renaming that folder (not deleting it) forces a fresh download on the next attempt. This is a system location — do it only if the earlier steps failed, and rename rather than delete so nothing is lost.

    Terminal renaming the /Library/Updates folder with sudo mv as a last-resort cache reset
    Step 6 — Renaming (not deleting) the update download cache to force a fresh download · img/m-02-s6.png

Verification and next steps

Success is visible progress: the percentage climbs again within minutes of whichever step you applied, or the download completes and offers installation. If the CLI list works but the GUI download still stalls after a fresh cache, collect what you have — the stuck state, the network test results, the CLI output — and take it to Apple Support; at that point the failure is specific enough to be worth their tools.

When this does not apply If the screen says the download finished and the problem is that installation never starts or never completes, you are in a different failure class — see tutorial 06 (downloaded but will not install) or tutorial 03 (frozen during installation).

03

Investigate an update that appears frozen during installation

A · Updates, upgrades & recoveryIntermediateIntel & Apple Silicon

During a major upgrade the installer rewrites the system volume, and several phases show no visible progress by design. The difference between “slow but working” and “genuinely stuck” is what the machine is doing underneath — and forcing power off at the wrong moment is the one move that can make a recoverable situation unrecoverable.

Problem and symptoms

The screen shows an installation in progress — a progress bar, a “Installing macOS” message, or a dark screen with a spinner — and it has not changed for what feels like too long. Major upgrades legitimately take well beyond the time people expect, particularly on large disks and older hardware, so elapsed time alone is a weak signal.

The uncomfortable part of this diagnosis is that during the install you cannot open Activity Monitor or Terminal to look under the hood — the installer occupies the machine. Your evidence has to come from what you can observe without touching it: whether the fans are working, whether an external backup drive's indicator is active (the updater may be reading or writing a snapshot), and how long ago the last visible change happened.

Likely causes and diagnosis

Three states look identical from the chair. In the first, the install is simply slow: large disks, cold caches and older storage make multi-gigabyte rewrites take a long time while the progress bar updates only at phase boundaries. In the second, the installer has genuinely wedged — a stalled write or a failed verification step — and will never finish on its own. In the third, something worse is happening: failing storage that cannot reliably complete the rewrite, in which case the right response is not patience but an immediate backup plan.

The practical decision rule: give the install a realistic time window for your Mac's age and disk size, watching for any sign of life (fan spin-up, drive activity). If it exceeds that window with no signs of activity at all, force a restart — on Apple Silicon Macs by holding the power button until the machine shuts down, on Intel Macs the same long-press applies. This is safe in the sense that macOS is built to fall back: if the new system volume failed early in its write, the machine typically reboots into the previous working system rather than a dead one. What you must not do is cycle power while you are unsure whether disk activity is ongoing.

Step-by-step solution

  1. Note when the install started, what the last visible state was, and set your expectations against the Mac's age and disk size. Write down a cutoff time — the point at which “no signs of life” becomes an action rather than waiting.

    Mac screen showing the macOS installation progress bar mid-upgrade
    Step 1 — The installation screen at its last visible state, with start time noted · img/m-03-s1.png
  2. Watch for passive signs of life without touching the machine: fan noise rising and falling, an external Time Machine drive's activity indicator, or any change in screen content. These indicate the system is still doing work even when the progress bar is static.

    External Time Machine backup drive with its activity light blinking while the Mac runs the upgrade
    Step 2 — External backup drive showing disk activity during the silent install phase · img/m-03-s2.png
  3. If the cutoff passes with no signs of life, force a restart: hold the power button down until the Mac shuts off, wait a few seconds, and power it on again. Do this only once you are confident nothing is actively writing.

    Hand holding the MacBook power button to force a shutdown after a confirmed dead install
    Step 3 — Forced restart after confirming no disk activity for the full cutoff window · img/m-03-s3.png
  4. After the reboot, check what actually happened. Open About This Mac: if it shows the new version, the install had completed and only the UI was stuck. If it shows the old version, the system fell back cleanly — your data should be intact; verify by opening a few recent files.

    About This Mac after reboot showing which macOS version actually booted
    Step 4 — About This Mac confirming whether the new or previous system booted · img/m-03-s4.png
  5. Run a storage health check either way. Open Disk Utility, select the internal disk, and run First Aid. A clean result supports retrying the upgrade; errors or repeated failures point at failing storage, which changes the entire plan (back up first, then consider hardware).

    Disk Utility running First Aid on the internal APFS container with the result panel open
    Step 5 — Disk Utility First Aid result for the internal disk after a stuck install · img/m-03-s5.png
  6. If storage checks out, retry the upgrade once from Software Update with the Mac on power. If it wedges again at roughly the same point, stop retrying — a repeat failure at the same stage is a stable fault worth escalating with the evidence you have gathered (stuck state, First Aid result, version before and after).

    Software Update screen ready for one careful retry of the major upgrade
    Step 6 — One deliberate retry of the upgrade after a clean storage check · img/m-03-s6.png

Verification and next steps

The outcome is verified by two facts: the booted version matches what you intended (new version installed, or clean fallback to the old one with data intact), and Disk Utility reports the internal disk as healthy. If the upgrade completes but something misbehaves afterwards, that is a separate problem class covered in tutorial 08; if the machine now loops back into the installer on every boot, move directly to tutorial 05.

Safety note Never force power off while you can still see disk activity (fans cycling, drive light blinking). Interrupting an in-progress system-volume rewrite is the one action in this tutorial that can turn a stalled update into data loss.

04

Diagnose repeated update failures and their error messages

A · Updates, upgrades & recoveryIntermediateIntel & Apple Silicon

When the same update fails identically on every attempt, the cause is stable and findable. The error text is not decoration — it names the stage where the pipeline broke, and working from that stage instead of retrying blindly is the difference between a fix and a loop.

Problem and symptoms

The update fails with an error dialog or message, you click Retry, and it fails again — sometimes with different wording, sometimes identical. After two or three cycles the situation has stopped being “an update that is having a bad day” and started being a reproducible fault with a specific cause.

The first discipline is to stop retrying before recording. Every failure attempt overwrites your best evidence (the exact message, the update name, the point where it stopped) with a new one you may not read carefully. Write down the verbatim error text and which update it names before you do anything else; if the dialog has already vanished, the Software Update screen's history area and the Console app both retain traces of recent failures.

Likely causes and diagnosis

Update failures split cleanly by stage, and the stage decides where to look. A download-stage failure (the transfer never completes or is interrupted) points at network conditions, proxy configuration, or a disk that ran out of room mid-transfer. An install-stage failure (the download finished but installation aborts) points at free space again — the installer needs headroom beyond the download itself — or at a corrupted staged payload, or in rarer cases at storage that cannot complete the rewrite.

Common macOS update failure symptoms, the likely stage they belong to, and the safe first check for each
What you seeLikely stageSafe first check
Download never reaches 100%, or errors mid-transferDownloadFree space, then network path (different connection)
Error mentioning not enough free space / storageInstall stagingStorage view; free space before retrying
Download completes, install aborts with a generic errorInstallFree space again (download occupies it), then one clean retry after restart
Same error text on every attempt, no progress at allStable local faultRecord verbatim; check Console logs for the software-update process around the failure time
Error only on this Mac, other Macs update fineLocal (space, disk, state)Free space and Disk Utility First Aid before anything else

The command-line update tool is the diagnostic workhorse here: it reports what Apple's catalog offers, and it installs with visible output instead of a silent GUI. If the CLI lists the update but installation still fails, its error text is more specific than the GUI's and belongs in whatever support conversation follows.

Terminal — list and install with visible output
# List what Apple's catalog offers this Mac (read-only).
softwareupdate -l

# Install available updates, printing progress and errors to Terminal.
# Requires admin; run only when you intend to install now.
sudo softwareupdate -ia

-l changes nothing. -ia actually installs — it is the same operation as the GUI button but with readable output, so a failure here gives you text worth quoting.

Step-by-step solution

  1. Record the failure verbatim: exact error text, the update name and version if shown, and which stage it stopped at (downloading vs installing). If the dialog is gone, open the Console app, filter on recent entries around the failure time, and locate the software-update process's messages.

    Console app log list showing software update process error entries with timestamps
    Step 1 — Console showing the update process's error entries for the failed attempt · img/m-04-s1.png
  2. Check free space in About This Mac → Storage. It is the most common install-stage cause and takes ten seconds to rule out or confirm.

    Storage settings showing available space on the startup disk
    Step 2 — Free-space check that rules in or out the most common install failure · img/m-04-s2.png
  3. Restart the Mac and retry the update exactly once. A restart clears transient state in the update process; one clean retry is a legitimate test, a fourth identical retry is not.

    Software Update screen after restart showing the same update ready for one clean retry
    Step 3 — One deliberate retry after a restart · img/m-04-s3.png
  4. If it fails again, switch to Terminal and run softwareupdate -l. An empty or erroring result means the catalog path is broken (network/proxy); a populated list confirms the update is reachable and local.

    Terminal showing softwareupdate -l output listing the available macOS update
    Step 4 — CLI catalog check confirming the update is reachable from this Mac · img/m-04-s4.png
  5. With a reachable update, attempt the install from Terminal with sudo softwareupdate -ia and let it run. Watch the output: where it stops and what it prints is now your precise failure description.

    Terminal showing sudo softwareupdate -ia progress output stopping with an error line
    Step 5 — CLI install attempt with the visible error line that names the failing step · img/m-04-s5.png
  6. Assemble the evidence for escalation or a deeper fix: verbatim GUI error, CLI output, free-space figure, and Console entries around the failure. At this point the problem is specific enough that generic advice (retry, restart) has been exhausted — what remains is either a targeted fix (space, payload cache per tutorial 02/06) or Apple Support with a real record.

    A text file summarizing the recorded error, CLI output and log entries for a support request
    Step 6 — The assembled failure record: error text, CLI output and log excerpts in one place · img/m-04-s6.png

Verification and next steps

Success is the update completing — from GUI or CLI — with the new version showing in About This Mac and no error on a final status check. If the failure persists with your evidence assembled, the next diagnostic step depends on what the CLI output named: storage errors route to Disk Utility First Aid and hardware consideration (tutorial 05's territory), while payload or state errors route to the targeted cache work in tutorials 02 and 06.

When this does not apply If there is no error message at all — the update simply stalls silently — the classification table above does not fit; use tutorial 02 (stuck downloading) or 03 (frozen installing) instead, which are built around absent rather than present errors.

05

Investigate a Mac that keeps returning to the Software Update screen

A · Updates, upgrades & recoveryAdvancedIntel & Apple Silicon

A machine that reboots straight back into the installer instead of your desktop is telling you the new system volume will not boot. The diagnostic question is why: a corrupted install is fixable from Recovery, but failing storage behind the same symptom is not — and only a disk check can tell them apart.

Problem and symptoms

You complete (or abandon) an update, the Mac restarts, and instead of your login screen it goes back to the update/installer environment. The second time this happens it is no longer a fluke: the boot process keeps landing in the same place because the volume it is trying to start from keeps failing that test.

It is worth understanding what macOS does here, because it shapes the diagnosis. During and after a major upgrade the system checks whether the new volume can boot; if it cannot, the machine falls back — either re-running the installer or booting the previous system where one still exists. A single fallback is normal self-repair. A loop means the condition that triggered the fallback has not changed, so nothing will be fixed by letting it cycle again.

Likely causes and diagnosis

Two causes dominate. The first is a corrupted or incomplete install: the new system volume was written but is damaged (interrupted write, bad payload), so it fails its boot check every time. This is recoverable — Recovery can run disk checks and reinstall macOS over the existing files without erasing them. The second is failing storage: the physical disk cannot reliably hold or verify the new volume. The symptom from the chair is identical, which is why this tutorial insists on a disk health check before any repair attempt: running First Aid produces either “clean, retry the install” or an error report that changes your entire plan to backup-first.

The diagnostic environment for this problem is macOS Recovery — the separate recovery volume that boots even when the main system will not. From there you get Disk Utility (for First Aid and container inspection) and a Terminal with enough of the system to run read-only checks. Everything below happens from that environment, because the normal desktop may be unreachable.

Step-by-step solution

  1. Let one full cycle complete and record exactly what happens at the end: does it show an error, a progress bar that dies, or a fallback to the old system? One observed cycle is your baseline; do not interrupt mid-cycle.

    Mac booting into the macOS installer/update screen instead of the login window
    Step 1 — The loop: Mac rebooting into the update screen instead of the desktop · img/m-05-s1.png
  2. Boot into Recovery deliberately. On Apple Silicon Macs, shut down and hold the power button until startup options appear, then choose Options; on Intel Macs, restart and hold Command-R (local recovery) or Option-Command-R on older models (internet recovery). You want the utility window with Disk Utility and Terminal.

    macOS Recovery utility window showing the Disk Utility, Terminal and reinstall options
    Step 2 — macOS Recovery environment loaded, ready for disk diagnostics · img/m-05-s2.png
  3. Run First Aid on the internal disk. In Disk Utility, select the top-level internal disk (the container, not just one volume), run First Aid, and read the result carefully: a clean pass supports an install retry; reported errors or repeated verification failures indicate storage trouble.

    Disk Utility in Recovery running First Aid on the internal APFS container with results shown
    Step 3 — First Aid result on the internal disk: clean, or errors that change the plan · img/m-05-s3.png
  4. Inspect the APFS structure read-only. In Recovery Terminal, run diskutil apfs list: it prints the container, its volumes and their sizes without changing anything. A container that reports healthy geometry with a damaged system volume is consistent with a bad install; odd or failing container-level information points at the hardware.

    Recovery Terminal showing diskutil apfs list output for the internal container and its volumes
    Step 4 — Read-only APFS container inspection from Recovery Terminal · img/m-05-s4.png
  5. If storage shows errors, stop repairing and start protecting: boot to the previous system if offered, or use another Mac to copy your data off (external drive, network), before any further attempt. A failing disk rewards patience with more loss; the backup is the only rational next action.

    Another Mac's Finder window copying files from an external drive holding the rescued data
    Step 5 — Data rescue in progress on a second Mac after storage errors were found · img/m-05-s5.png
  6. If storage checks out, reinstall macOS from Recovery without erasing: choose Reinstall macOS and let it run to completion over the existing files. This rebuilds the system volume while keeping user data — the correct repair for a corrupted install on healthy hardware.

    Reinstall macOS option in Recovery beginning an install over the existing files
    Step 6 — Non-erasing reinstall of macOS from Recovery on a disk that passed First Aid · img/m-05-s6.png

Verification and next steps

The loop is broken when the Mac reaches your login screen at the intended macOS version and stays there across a full shutdown-and-power-on cycle — one reboot is not proof, two are. If the reinstall completes but the loop returns, the storage verdict from First Aid should be revisited with fresh eyes (and ideally on Apple's own diagnostic hardware), because a disk that passes a quick check can still fail under sustained write load.

When this does not apply A single fallback to the previous system after a failed upgrade is designed behavior, not a loop — if the desktop returns and works, treat it as tutorial 10's territory (planning what to do next), not this one.

06

Fix an update that downloads completely but never installs

A · Updates, upgrades & recoveryIntermediateIntel & Apple Silicon

The download finished — that part is done. What remains is the installer's own gate: it will not start (or will not finish) when free space ran out after the download landed, a restart was required and missed, or the staged payload is corrupt. Each has a specific test.

Problem and symptoms

Software Update shows the update as downloaded — 100%, “Install” offered, or a “restart to finish installing” prompt that never materializes. Clicking Install does nothing visible, or the install begins and aborts without an error you can act on. The machine is not downloading anything; it is sitting at the handoff between download and installation.

This state confuses people because the progress bar did its job — the failure is invisible until you look at the conditions the installer actually checks before starting. The two that matter most: free space (the downloaded payload itself now occupies several gigabytes, so a disk that had room for the download may have none left for staging the new volume), and pending state (some updates require a restart to complete their handoff, and a Mac that was never restarted sits holding the half-finished transaction).

Likely causes and diagnosis

Work the three causes in order of likelihood. First, space consumed by the download: re-check storage now, not as it was yesterday — if available space is minimal, this is your cause and freeing space (category C) unblocks the install. Second, a missed restart: if the update ever showed a restart prompt and the Mac kept running, the handoff never completed; one reboot often resumes it automatically. Third, a corrupt staged payload: less common, but when space is fine and a restart changed nothing, the cached install data may be damaged — in which case forcing a fresh attempt via the command line (and, as a last resort, clearing the download cache per tutorial 02) is the path.

The command-line installer is useful here for a second reason: unlike the GUI button, it reports why it refuses to proceed. If softwareupdate -ia declines or aborts with text, that text is the diagnosis; if it simply works, you have your fix and the GUI state was stale.

Step-by-step solution

  1. Re-check free space right now in About This Mac → Storage. Compare it with what it was before the download: if the difference roughly equals the update's size and what remains is thin, space is your cause.

    Storage settings showing reduced available space after the multi-gigabyte update download completed
    Step 1 — Storage view showing how much room the finished download left for installation · img/m-06-s1.png
  2. If space is tight, free it using the safe categories first: caches, build artifacts and local snapshots (tutorials 28–32). Do not delete personal files to make room for an update — that trades a fixable problem for a permanent one.

    Storage management suggestions screen showing reclaimable items such as caches and large files
    Step 2 — Storage management view identifying what can safely be reclaimed · img/m-06-s2.png
  3. Restart the Mac once, even if no restart prompt was shown. On return, check Software Update: a pending handoff often resumes automatically and begins installing without further input.

    Software Update screen after reboot showing the update resuming installation automatically
    Step 3 — After restart, the pending install handoff resumes on its own · img/m-06-s3.png
  4. If nothing resumed, open Terminal and run sudo softwareupdate -ia. Watch the output: a refusal message names the blocking condition; a running install means the GUI state was simply stale.

    Terminal showing sudo softwareupdate -ia beginning to install the already-downloaded update
    Step 4 — CLI install attempt with visible output instead of a silent GUI button · img/m-06-s4.png
  5. If the CLI also fails and space is genuinely fine, treat the staged payload as suspect: rename the update download cache (the last-resort step in tutorial 02) and let the update re-download once. A fresh payload rules out corruption of the cached file.

    Terminal renaming the /Library/Updates cache folder to force a fresh download of the staged payload
    Step 5 — Cache renamed so the next attempt re-downloads a clean payload · img/m-06-s5.png
  6. Retry the install (GUI or CLI) after the fresh download completes. If it still will not proceed, you now have a specific record — space figures, restart result, CLI output, cache reset done — which is exactly what Apple Support needs to go deeper.

    Software Update screen after a fresh download showing the Install button ready for the final attempt
    Step 6 — Final install attempt after payload refresh, with all evidence recorded · img/m-06-s6.png

Verification and next steps

The fix worked when the installation actually runs to completion: progress moves through install phases, the Mac restarts into the new version, and About This Mac confirms it. If the install starts but then freezes mid-installation, you have crossed into tutorial 03's territory — apply its wait-then-assess method rather than interrupting blindly.

When this does not apply If the download itself never reaches 100%, this tutorial's gates are irrelevant — the problem is upstream, in tutorial 02 (stuck downloading).

07

Tell an unsupported Mac from a compatible one with a fixable problem

A · Updates, upgrades & recoveryBeginnerIntel & Apple Silicon

When Software Update offers no new macOS at all, there are only two explanations: this model is not on the release's support list, or a compatible Mac is failing to see the catalog. Ten minutes of checking settles which one you have — and the answers lead to completely different next steps.

Problem and symptoms

Software Update says your Mac is up to date, or simply shows no new major version, while newer macOS releases exist. The question people actually face: am I out of luck with this machine, or is something fixable preventing a legitimate update from appearing?

The distinction matters because the outcomes are different in kind. An unsupported Mac will never receive that release — no setting, reset or repair changes Apple's support list, and the honest response is a planning decision (security exposure, replacement timing). A compatible Mac that cannot see the catalog has a concrete fault — network path, free space, or update state — and a concrete fix.

Likely causes and diagnosis

Start with the fact that settles most cases: Apple publishes, for each macOS release, the list of Mac models it supports. Your Mac's exact model appears in About This Mac (the identifier under More Info, a code like a letter-number series). If your model is on the list for the release you want, the Mac is compatible and the missing offer is a local problem. If it is not on the list, no amount of local troubleshooting will produce the update — that is the design, not a bug.

For the compatible-but-silent case, the usual causes are the same family as tutorial 02's: the network path to Apple's update catalog is blocked or failing (proxy, firewall, ISP), free space is below what the release needs, or the local update state is stale. The command-line check separates these quickly: if softwareupdate -l returns a list, the catalog is reachable and the GUI state is the problem; if it errors, the network path is.

Terminal — is the catalog reachable at all?
# Read-only: lists updates Apple's catalog offers this Mac.
softwareupdate -l

A populated list on a Mac whose GUI shows nothing means the offer exists and local state is stale — a restart often restores it. An error or hang points at the network path.

Step-by-step solution

  1. Find your exact model identifier: Apple menu → About This Mac, then More Info (or System Information) to reveal the full model code. Write it down — support lists are matched against this code, not marketing names.

    About This Mac window expanded to show the model identifier line
    Step 1 — The exact model identifier, which is what Apple's support lists are checked against · img/m-07-s1.png
  2. Check Apple's supported-models page for the macOS release you want and search for your model code. This is the authoritative test — not forum posts, not the Mac's age alone.

    Apple's website listing the Mac models supported by a specific macOS release
    Step 2 — Apple's official support list for the target release, searched for your model code · img/m-07-s2.png
  3. If your model is absent: accept that this release will not come to this Mac. The installed version remains usable but stops receiving updates, which is a security consideration worth weighing deliberately (sensitive work on an unpatched OS) alongside a replacement plan. No local fix exists for this case.

    Software Update screen showing the Mac is up to date with no newer major version offered
    Step 3 — The up-to-date state of an unsupported Mac: stable, but no further updates will appear · img/m-07-s3.png
  4. If your model is supported, check free space in Storage — a release that needs more room than you have will not be offered. Then run softwareupdate -l in Terminal to test catalog reachability directly.

    Terminal showing softwareupdate -l returning the available macOS version for a supported Mac
    Step 4 — CLI confirming the update exists and is reachable on a supported Mac · img/m-07-s4.png
  5. If the CLI lists the update but the GUI does not show it, restart the Mac and re-check Software Update. Stale local state clears on reboot in most cases; if the offer still does not appear, the network path (proxy/firewall) deserves a test from a different connection.

    Software Update screen after restart now offering the major macOS version
    Step 5 — The missing offer appearing after the stale state cleared · img/m-07-s5.png
  6. Once the update is visible, prepare and install it using tutorial 01's routine. The diagnosis here ended in one of two places — a planning decision or a working offer — and both are now on record.

    Software Update screen with the update ready to install after preparation steps
    Step 6 — Update visible and ready, following the standard preparation routine · img/m-07-s6.png

Verification and next steps

You are done when you can state one of two facts with evidence: this model is not supported for that release (support-list screenshot plus your model code), or the update is now offered and installable (visible in Software Update or listed by the CLI). In the second case, continue with tutorial 01's preparation before installing; in the first, the next step is a decision, not a repair.

When this does not apply If the update is offered and visible but fails during download or install, support status is no longer the question — use tutorials 02–04 for the failure itself.

08

Investigate Wi-Fi, peripheral and app problems that appear after an update

A · Updates, upgrades & recoveryIntermediateIntel & Apple Silicon

A major upgrade changes the system frameworks underneath everything else, and some things need a conscious re-approval or a vendor's updated build before they work again. Sorting “the OS broke it” from “this app needs its own update” saves you from rolling back an operating system to fix one program.

Problem and symptoms

The upgrade completed, the desktop looks normal — and then pieces stop working: Wi-Fi drops or will not connect, a Bluetooth device no longer pairs, an app crashes on launch or loses features. Individually each symptom could be its own fault; together, appearing right after an upgrade, they share a common context worth exploiting.

The key diagnostic fact is that an upgrade does not break everything at once — it changes specific layers. Third-party system extensions (the modern replacement for kernel extensions) must be re-approved by you after an upgrade, and until you approve them the software built on them sits disabled. Application vendors ship compatibility updates over days or weeks after a new macOS lands. Network and Bluetooth stacks are part of the OS itself and usually survive upgrades intact — which means a Wi-Fi failure immediately after an update is more often configuration or hardware than framework breakage.

Likely causes and diagnosis

Sort each symptom into one of three buckets before touching settings. Approval-pending software: check System Settings → Privacy & Security — after upgrades, a notice may sit there saying a system extension from a named developer is waiting for approval, with an Allow button. Until you click it, that vendor's tools (disk utilities, virtualization, security agents) will misbehave or vanish. Vendor-side compatibility: if one or a few apps fail while everything else works, the cause is almost certainly in those apps; the fix is their updated version, not your system. OS-level connectivity: Wi-Fi and Bluetooth problems deserve direct checks — is the interface still present and configured? Do other Macs on the same network behave normally? A re-paired device or a reconfigured network profile often resolves post-upgrade connectivity quirks.

One caution shapes this whole category: do not reach for disabling security protections (Gatekeeper, System Integrity Protection) as a way to force old software to run after an upgrade. Those controls exist precisely because pre-upgrade binaries and extensions are the risky ones; re-approval prompts and vendor updates are the designed path, and the secure fix is almost always available within days.

Step-by-step solution

  1. Check Privacy & Security for pending approvals. Open System Settings → Privacy & Security and look for a notice about a system extension waiting for approval; if the named developer is one you installed deliberately, click Allow and restart the affected app.

    Privacy and Security pane showing a pending system extension approval notice with the Allow button
    Step 1 — The post-upgrade system-extension approval prompt that silently disables vendor tools until accepted · img/m-08-s1.png
  2. Verify the Wi-Fi interface is still present and configured. Open System Settings → Wi-Fi (and Network settings if needed): the interface should exist with your network profile intact. A missing or reset interface after an upgrade is rare and points at configuration or hardware, not frameworks.

    Wi-Fi settings showing the network list and the current connection status after the update
    Step 2 — Wi-Fi settings confirming the interface and known networks survived the upgrade · img/m-08-s2.png
  3. If Wi-Fi misbehaves, test against a baseline: connect another device (phone or second Mac) to the same network. If others work fine, the problem is this Mac's configuration — try forgetting and rejoining the network, which rebuilds the local profile cleanly.

    The forget-network confirmation dialog for a Wi-Fi network being reconfigured after an update
    Step 3 — Forgetting and rejoining the network to rebuild its local configuration · img/m-08-s3.png
  4. Re-pair Bluetooth devices that stopped connecting: forget each affected device in Bluetooth settings, then pair it again. Post-upgrade pairing failures are usually stale pairing state, not broken hardware — a clean re-pair tests exactly that.

    Bluetooth settings showing a device being forgotten and re-paired after the update
    Step 4 — Bluetooth device forgotten and re-paired to clear stale pairing state · img/m-08-s4.png
  5. For failing applications, check for vendor updates first. Open the App Store's Updates page and visit each affected app's site or update mechanism; post-upgrade crashes are most often fixed by a new build of the app, and forcing the old build to run is both harder and less safe.

    App Store updates page showing pending updates for applications after a macOS upgrade
    Step 5 — App Store updates available for the affected applications · img/m-08-s5.png
  6. Re-test each symptom after its specific fix, and record which bucket it fell into. Symptoms that survive their bucket's fix (Wi-Fi still broken after reconfiguration, an app still crashing after its update) have outgrown the post-upgrade explanation and deserve their own diagnosis.

    A checklist noting which post-update symptoms were resolved by approval, re-pairing or app updates
    Step 6 — Symptom-by-symptom record of what each fix resolved · img/m-08-s6.png

Verification and next steps

The post-upgrade state is verified when: pending approvals are consciously accepted or declined, Wi-Fi connects and survives a reboot, paired peripherals reconnect without intervention, and the affected apps run their core functions after vendor updates. Anything still broken at that point should be diagnosed on its own merits — with the upgrade noted as context in the record (tutorial 52's format), not assumed as cause.

When this does not apply If the same Wi-Fi or peripheral problems existed before the update, the upgrade is a coincidence in your timeline — diagnose the connectivity or hardware issue directly (the site's networking and hardware topic pages) rather than working through this post-upgrade path.

09

Decide when a problem needs Safe Mode, Recovery, or ordinary troubleshooting

A · Updates, upgrades & recoveryIntermediateIntel & Apple Silicon

Safe Mode and Recovery are powerful, but each changes a different set of conditions — and using the wrong one wastes an hour while masking the real test. This tutorial is the decision aid: what each environment actually does, when it is the right tool, and how to enter each on both Apple Silicon and Intel Macs.

Problem and symptoms

You have a problem that ordinary troubleshooting has not resolved — something misbehaving after an update, a suspected third-party extension, a disk you need to check, or a system you may need to reinstall. The question is which environment to work in, because the answer determines what evidence the test can produce.

The confusion is understandable: both Safe Mode and Recovery are “special boot modes,” and guides often treat them as interchangeable. They are not. Each one changes a specific subset of what runs on your Mac, and a problem that disappears in one environment but not the other localizes the cause to whatever differs between them. That is the entire diagnostic value — and it only works if you know exactly what each mode turns off.

Likely causes and diagnosis

Here is what each environment actually does, which is the basis for the decision:

macOS special boot environments: what each one changes, when it is the right tool, and how to enter it on Apple Silicon and Intel Macs
EnvironmentWhat it changesUse it when…How to enter
Normal startupEverything runs, including all third-party extensions and login itemsAlways first — most problems resolve without any special modeStandard power-on
Safe ModeThird-party kernel/system extensions do not load; a basic disk check runs; the font cache is rebuilt; some startup behavior is skippedA problem appeared after installing third-party software, or you suspect an extensionApple Silicon: shut down, hold power button until options appear, select your volume and choose Safe Mode (via Options/Gear); Intel: restart and hold Shift during startup
macOS RecoveryBoots a separate recovery volume instead of your system: Disk Utility, Terminal, macOS reinstall — your main system is not runningYou need disk tools, a non-erasing reinstall, or the normal system will not startApple Silicon: shut down, hold power button until startup options appear, choose Options; Intel: restart and hold Command-R (older models: Option-Command-R for internet recovery)
Apple DiagnosticsRuns hardware-level self-tests outside the operating systemYou suspect failing hardware (storage, memory) rather than softwareApple Silicon: from the startup options screen, press and hold the Option key; Intel: restart and hold D

The diagnostic logic follows directly. If a problem vanishes in Safe Mode, the cause is almost certainly third-party software that Safe Mode excludes — the next step is identifying which recently added item it is, not staying in Safe Mode forever (it is a test environment, with reduced functionality). If the problem persists in Safe Mode, third-party extensions are largely ruled out and suspicion moves to the system itself or hardware. And Recovery is not a “stronger Safe Mode” — it is a different tool for jobs Safe Mode cannot do: disk repair, reinstalling macOS, or reaching a machine whose main system will not boot.

Step-by-step solution

  1. Exhaust the ordinary path first: restart normally, re-test, check the specific settings or logs the symptom points at. Special modes are for problems that survive normal troubleshooting — reaching for them early burns time and muddies the diagnosis.

    A normal macOS desktop after a standard restart, re-testing the reported problem
    Step 1 — The problem re-tested after an ordinary restart, before any special mode is considered · img/m-09-s1.png
  2. If third-party software is a suspect (recently installed extension, driver, or system utility), boot into Safe Mode using the method for your architecture from the table above. Note that entering Safe Mode on Apple Silicon Macs goes through the startup-options flow rather than a key hold during chime.

    Apple Silicon startup options screen with the volume selected and the Safe Mode option visible
    Step 2 — Apple Silicon startup options showing how Safe Mode is selected · img/m-09-s2.png
  3. In Safe Mode, re-run the exact test that produced the original symptom. Record the result precisely: gone, reduced, or unchanged. This single observation splits your suspect list in half — third-party software on one side, system/hardware on the other.

    The previously failing application being re-tested while running in Safe Mode
    Step 3 — The original failing test repeated in Safe Mode to localize the cause · img/m-09-s3.png
  4. If the problem is gone in Safe Mode, return to normal startup and identify the culprit by elimination: recently added apps and system utilities first. Remove or disable one suspect at a time, testing after each change — never strip everything at once, or you will not know which item was responsible.

    Applications folder showing recently installed utilities being reviewed one by one
    Step 4 — Reviewing recently added software to isolate the item Safe Mode excluded · img/m-09-s4.png
  5. If you need disk tools, a reinstall, or the system will not start at all, boot into Recovery instead (table above). From there, Disk Utility's First Aid and the non-erasing Reinstall macOS option cover most recovery jobs without touching user data.

    Recovery environment with Disk Utility open on the internal disk for a First Aid check
    Step 5 — Recovery environment loaded for disk-level work that Safe Mode cannot do · img/m-09-s5.png
  6. If the problem persists in both Safe Mode and normal operation, and disk checks pass, run Apple Diagnostics (table above) to test the hardware layer. A clean diagnostic result on persistent symptoms points back at software configuration — documented per tutorial 52 — rather than a failing component.

    Apple Diagnostics screen running its hardware self-test with the reference code displayed
    Step 6 — Apple Diagnostics hardware test as the final layer of this decision tree · img/m-09-s6.png

Verification and next steps

The decision is complete when you can state where the cause lives: third-party software (problem absent in Safe Mode, culprit identified by elimination), system-level (persists in Safe Mode, disk healthy — configuration or OS repair work), or hardware (diagnostic errors). Each branch has a concrete next step, and the record of which environment changed what is worth keeping even after the fix, because it predicts how the class of problem will behave next time.

When this does not apply If the Mac cannot reach any desktop at all and loops into the installer, you are already in Recovery territory by necessity — tutorial 05 covers that sequence; this tutorial is for machines that still boot normally but misbehave.

10

Plan a rollback or recovery strategy when an update goes seriously wrong

A · Updates, upgrades & recoveryAdvancedIntel & Apple Silicon

“Can I just go back?” is the first question after a bad upgrade — and macOS's honest answer is: sometimes, but only through specific routes with specific preconditions. This tutorial maps what rollback actually means on a Mac, which routes exist, what each one depends on, and how to plan the one that fits your situation.

Problem and symptoms

A major update has caused a serious problem: critical software no longer works, performance has collapsed, or the system is unstable enough that daily work has stopped. The instinct is to reverse the upgrade, and the planning question is whether reversal is available to you at all — because unlike a software install, an operating-system version change does not have an uninstall button.

It helps to understand why. A major upgrade replaces system components across the whole disk, and Apple does not ship a “downgrade” mechanism for it. What macOS does provide is a set of recovery routes that can return you to a working state — some of them at the previous version, some at the current one with a repaired installation. Which routes are open depends on facts about your machine: whether a pre-upgrade snapshot survived, whether you have a complete Time Machine backup from before the update, and which hardware generation you are on.

Likely causes and diagnosis

The available routes, in order of preference (least destructive first):

  • Automatic fallback to the previous system. If the new version fails early in its installation or boot, macOS can fall back to the pre-upgrade snapshot it keeps for exactly this purpose. This happens without user action on many machines — but its availability varies by macOS version and hardware, so it must be treated as a possibility, not a guarantee.
  • Booting the previous system from Recovery. Where a pre-upgrade snapshot exists, Recovery's boot options can sometimes select it directly. This is non-destructive to your data and is the cleanest true rollback — when it is available at all.
  • Full restore from a pre-update Time Machine backup. The route that always works if the precondition holds: erase the disk in Recovery and restore from a Time Machine backup taken before the update. This returns both the old macOS version and the state of your data as of that backup — at the cost of anything created after the backup point.
  • Reinstalling the current (new) version without erasing. Not a rollback, but often the right outcome: a fresh system volume over existing files fixes corrupted installs while keeping you on the new release. Choose this when the problem is the installation, not the version.
  • Internet Recovery to the latest supported version. A last resort that reinstalls the newest macOS your hardware supports — never an older one. Useful for a destroyed system volume; useless as a downgrade.

The diagnosis step is therefore an inventory of preconditions, not a guess about what should work: Does a pre-upgrade snapshot exist (Recovery's boot options will show it if so)? Is there a Time Machine backup from before the update, and has it been verified as complete? What exactly broke — the installation (reinstall may suffice) or the version itself (rollback needed)? Answering those three questions selects your route.

Step-by-step solution

  1. Inventory the damage precisely: what stopped working, when it started relative to the update, and whether the current system is stable enough to keep using for a while. A system that still boots and runs your backup software is in a much stronger position than one that does not.

    A written record listing what broke after the update and when each problem started
    Step 1 — The damage inventory: symptoms, timing relative to the update, current stability · img/m-10-s1.png
  2. Boot into Recovery and check what boot options exist. If a pre-upgrade system snapshot is offered as a startup volume, that is your cleanest rollback path — select it and test whether the previous version boots with your data intact.

    Recovery startup options listing the available volumes including a pre-upgrade system snapshot
    Step 2 — Recovery's volume list showing whether a pre-upgrade snapshot is available to boot · img/m-10-s2.png
  3. If no previous-version route exists, verify your Time Machine backup from before the update. From Recovery (or another Mac), browse the backup and confirm it contains recent pre-update data — a restore plan built on an unverified backup is not a plan.

    Time Machine browser in Recovery showing the pre-update backup's file history being verified
    Step 3 — Verifying the pre-update Time Machine backup contains complete, recent data · img/m-10-s3.png
  4. Decide and document the route. If a previous-version boot worked: stay there, restore anything created after that point from other sources, and treat the new version as abandoned for now. If not: schedule the full erase-and-restore from Time Machine, noting exactly what data created after the backup point will be lost (and rescuing it to an external drive first, if the current system still runs).

    An external drive being used to rescue files created after the last good backup point
    Step 4 — Rescuing post-backup data to an external drive before any erase-based restore · img/m-10-s4.png
  5. Execute the chosen route. For a Time Machine restore: in Recovery, use Erase Disk and then choose Restore from Time Machine Backup, following the prompts to completion. This is the one destructive step in this tutorial — it erases the current disk, which is why steps 3 and 4 exist before it.

    Recovery's Restore from Time Machine Backup screen beginning the full restore process
    Step 5 — The erase-and-restore in progress from Recovery, after backup verification and data rescue · img/m-10-s5.png
  6. After whichever route completes, verify the outcome against your inventory: the intended macOS version is running, the problems from step 1 are gone or reduced, and your data matches what the chosen source should contain. Record the route taken and its result — this is now a tested fact about this machine, not folklore.

    About This Mac after recovery showing the restored macOS version with the problem list checked off
    Step 6 — Post-recovery verification: version confirmed, original problems re-tested · img/m-10-s6.png

Verification and next steps

The strategy is complete when the machine runs a stable system at a version where your work can continue, with data verified against its source. If you landed back on the new version via a non-erasing reinstall and problems persist, the version itself is the issue for that software — and the realistic options are waiting for vendor fixes or accepting the limitation, since further rollback routes have been exhausted by design.

Safety note The erase-and-restore route destroys everything currently on the disk. It is only safe after the backup has been browsed and verified (step 3) and post-backup data has been rescued (step 4). No recovery procedure in this library should ever be run ahead of those two checks.

When this does not apply If the “serious problem” is one misbehaving application while the system itself is stable, rolling back the operating system is disproportionate — fix or replace the app (tutorial 08's vendor-update path) and keep the working system.

§04

Category B · 12 tutorials

Privacy, permissions & access

macOS privacy protection is two layers most people conflate: Unix file permissions (who owns a file) and TCC privacy grants (what an app may touch). These tutorials separate the layers and walk each grant’s diagnosis path — from Full Disk Access to permission prompts that never stop repeating.

11

Understand macOS privacy permissions: what each one actually governs

B · Privacy, permissions & accessBeginnerIntel & Apple Silicon

macOS asks for permission in ways that look interchangeable — a dialog, a settings list, an “Allow” button — but each permission governs a different layer of access. Knowing which layer your problem lives in is what turns a random search through Privacy settings into a two-minute fix.

Problem and symptoms

An application cannot do something you expect: it will not read your documents, an automation tool cannot click buttons, a screen capture comes out blank, a terminal command is refused. The common thread is that macOS privacy protections sit between the app and the resource, and each protection has its own name, its own settings location, and its own failure signature.

Two facts frame everything in this category. First, these permissions are managed per application (and, on recent versions, some per specific folder), stored by the system's privacy framework — you grant or revoke them in System Settings → Privacy & Security, and the exact set of lists and their names has shifted across macOS versions, so treat screenshots from older releases with care. Second, granting one permission does not imply the others: Full Disk Access is broad but still not a universal key (more on that in tutorial 12), and an app can hold every folder permission it needs yet still be blocked by Accessibility or Screen Recording for its other functions.

Likely causes and diagnosis

The table below is the map. When a denial happens, identify which row describes the access being refused — that row's settings list is where the fix lives, and that row's typical symptom tells you whether you are even looking at the right layer.

macOS privacy permission types, the access each one governs, and the typical symptom when it is missing
Permission (Privacy & Security list)What it governsTypical denial symptom
Full Disk AccessBroad read/write access to protected data the system shields from ordinary apps: backups, mail and message stores, browser data, other apps' containers“Operation not permitted” on specific protected folders even when folder permissions are granted; tools that need wide access (backup agents, dev tools) failing silently
Files and FoldersPer-folder access for individual apps: Desktop, Documents, Downloads, and any folder you explicitly shareAn app cannot open files in one specific location; a prompt appears the first time it touches that folder and was denied or never answered
AccessibilityThe ability to control other applications: simulating input, reading UI elements, automating clicks and keystrokesAutomation tools (macro apps, accessibility utilities) launch but cannot act on other apps; no error, just nothing happens
Screen RecordingCapturing the screen or window content; on recent versions a related option covers system audio captureCaptures are blank or black; virtual desktops and remote-viewing tools show an empty screen
Camera / MicrophoneAccess to the built-in (or connected) camera and microphone hardwareVideo or audio apps see no input device; the indicator light does not turn on
Location ServicesAn app's access to the Mac's locationLocation-based features report no position, or the app asks repeatedly without remembering a grant
Input MonitoringObserving keystrokes and input events system-wide (distinct from controlling apps via Accessibility)Typing-analysis tools, some input method enhancements, or key utilities see no input even when installed
AutomationOne app's ability to send commands to another specific app (Apple Events)Scripted workflows fail at the handoff between apps; a prompt names both the controlling and controlled application
App ManagementInstalling, updating or removing other applications on the systemInstaller tools and software updaters cannot write to /Applications without manual intervention

One more distinction prevents a common misdiagnosis: these privacy permissions are separate from Unix file permissions (the owner/group/other model you see with ls -l). A file can be readable by your user at the Unix level and still be refused by macOS's privacy layer for a specific app — and conversely, a Unix “permission denied” is usually not a privacy-list problem at all. Tutorial 18 separates the two in depth; for now, the rule of thumb is that privacy denials name the application, while Unix denials name the file.

Step-by-step solution

  1. Reproduce the denial and read it carefully. Note exactly what the app could not do, which resource was involved (a folder, the screen, another app, a device), and whether macOS showed a prompt or failed silently. The resource tells you which row of the table applies.

    An application showing an error about being unable to access a specific folder
    Step 1 — The denial as observed: app, resource and failure mode noted before opening any settings · img/m-11-s1.png
  2. Map the resource to its permission row using the table. A specific folder points at Files and Folders (or Full Disk Access for protected locations); controlling another app points at Accessibility or Automation; blank captures point at Screen Recording.

    The privacy permission table from this tutorial next to the observed denial, matching resource to permission type
    Step 2 — Matching the observed resource to the governing permission type · img/m-11-s2.png
  3. Open System Settings → Privacy & Security and locate that specific list. Check whether your application appears in it at all: absent means it was never granted (or the grant was lost — see tutorial 21); present but toggled off means it was revoked.

    Privacy and Security settings showing one of the permission lists with apps and their toggle states
    Step 3 — The relevant permission list in Privacy & Security, showing which entries exist · img/m-11-s3.png
  4. Apply the smallest grant that matches the need: enable the specific entry for the failing app, or add it if absent (the list's add control accepts an app from your Applications folder). Resist granting Full Disk Access “just in case” — if a narrower permission fits the failure, use that one.

    Adding an application to a specific privacy permission list via the add control
    Step 4 — The targeted grant applied: the failing app added to the one list that governs its need · img/m-11-s4.png
  5. Restart the application fully (quit and relaunch, not just close windows) and re-run the operation that failed. Many privacy grants take effect only on the app's next launch — testing without a restart produces false negatives.

    The application relaunched and successfully performing the previously denied operation
    Step 5 — The failing operation re-run after a full app restart, now succeeding · img/m-11-s5.png
  6. If the operation still fails after the correct grant and a restart, you are in a different layer: check for a second required permission (some tools need two — e.g., Screen Recording plus Accessibility), then move to tutorial 18 to rule out Unix-level permissions or protected-path behavior.

    Two permission lists checked side by side for an app that requires both grants
    Step 6 — Checking for the second required permission when one grant was not enough · img/m-11-s6.png

Verification and next steps

The fix is verified when the previously denied operation succeeds after a full app restart, and the grant you made is the narrowest one that achieved it. If denials keep recurring for the same app after correct grants, tutorial 16 covers why permissions get lost (updates, reinstalls, signature changes); if the denial text is specifically “Operation not permitted” on protected data, tutorial 12's Full Disk Access workflow is the next step.

12

Grant and verify Full Disk Access for a trusted application

B · Privacy, permissions & accessBeginnerIntel & Apple Silicon

Full Disk Access is the broadest routine privacy grant macOS offers, and the one most often needed by backup tools, developer utilities and system maintenance software. Granting it correctly — to the right binary, verified with a real test — takes five minutes; granting it carelessly is how machines accumulate access they no longer need.

Problem and symptoms

A trusted application cannot reach data that is clearly yours: a backup tool skips your Mail or Messages folders, a developer utility reports “Operation not permitted” on specific paths, or a disk-cleanup app refuses to touch certain directories. The files are readable by you in Finder — the block is macOS's privacy layer shielding particular data stores from apps that have not been explicitly trusted for wide access.

Full Disk Access (FDA) is the permission designed for exactly this: it lifts the privacy-layer shield for the named application across the protected data categories. Two boundaries matter before you grant it. First, FDA does not automatically override every other security boundary on the system — it is a privacy-permission grant, not a root credential; System Integrity Protection still guards system files, and some access paths have their own controls. Second, the grant attaches to a specific application binary (identified by its code signature), so an updated or reinstalled app can lose it — which is why verification matters as much as granting.

Likely causes and diagnosis

Before touching FDA, confirm you are actually in the FDA class of problem. The signature is specific: the failure names protected data (mail stores, message databases, backup volumes, browser profiles, other apps' containers) and typically reports “Operation not permitted” rather than a plain permission error. If instead the app cannot read one ordinary folder like Documents, that is the Files and Folders layer (tutorial 13), and FDA would be an unnecessary escalation.

Also identify which binary needs the grant. Some applications run helper processes or separate utilities that are distinct privacy subjects — granting FDA to the main app while the failing operation happens inside a helper leaves the problem in place. The prompt macOS shows (when it prompts at all) names the requesting item; when there is no prompt, the application's own error message usually identifies which component failed.

Step-by-step solution

  1. Confirm the app is one you deliberately installed and trust — FDA is a broad grant, and the only responsible question to ask before adding an entry is whether this specific binary earns it. If you cannot say why the app needs wide access, stop and investigate the need first.

    The application's about box or documentation stating why it requires Full Disk Access
    Step 1 — Confirming which application (and which component) actually needs the grant · img/m-12-s1.png
  2. Open System Settings → Privacy & Security → Full Disk Access. Check whether the app is already listed: if present but off, the fix is simply enabling its toggle. If absent, use the list's add control to select the application from your Applications folder.

    The Full Disk Access list in Privacy and Security showing existing entries and their toggles
    Step 2 — The Full Disk Access list before the change: checking for an existing entry first · img/m-12-s2.png
  3. Add or enable the entry, then quit the application completely and relaunch it. FDA changes apply when the app next starts; testing in a running instance gives false results and wastes the diagnosis.

    The newly added application shown enabled in the Full Disk Access list
    Step 3 — The grant applied: the app present and toggled on in Full Disk Access · img/m-12-s3.png
  4. Run a deliberate test of the operation that originally failed — the same folder, the same action, not a different one that happens to work. The test should touch genuinely protected data (for example, listing the contents of a mail or messages store) so you are verifying the grant, not just the app.

    Terminal listing a protected data directory successfully after Full Disk Access was granted to the terminal app
    Step 4 — The deliberate verification test: the previously refused operation now completing · img/m-12-s4.png
  5. If the test still fails, check whether the failing component is a different binary than the one you granted (helper tools, command-line front ends). Grant FDA to that specific item as well — each privacy subject needs its own entry.

    A helper utility being added separately to the Full Disk Access list alongside its parent app
    Step 5 — Granting the second component (helper binary) when the main-app grant was not enough · img/m-12-s5.png
  6. Note the grant in your own records: which app, why, and when. FDA entries do not show their reason inside macOS, so a future you (or tutorial 22's audit) will thank present you for the note.

    A personal notes file listing granted Full Disk Access entries with their reasons and dates
    Step 6 — The grant recorded with its reason, ready for a future audit · img/m-12-s6.png

Verification and next steps

Success is the specific operation that failed now succeeding after a full app restart, with the grant limited to the components that genuinely need it. If the app loses access again later without any change on your side, tutorial 16 explains the signature-change mechanism behind recurring prompts; if you would like to review what FDA entries currently exist and trim them, tutorial 22 is the audit workflow.

Safety note Full Disk Access is a trust decision, not a technical one. Every entry you add should be an application you installed deliberately and can name the reason for — when in doubt during this process, leave the entry out and investigate the app's actual need instead.

13

Fix an app that cannot access Desktop, Documents, Downloads or another folder

B · Privacy, permissions & accessBeginnerIntel & Apple Silicon

macOS treats your Desktop, Documents and Downloads as protected locations: apps must be granted access to them specifically, and a single denied prompt years ago can still be the reason an app cannot see files today. The fix is usually one toggle — if you can find the right entry for the right binary.

Problem and symptoms

An application cannot open, save to, or even list files in one of your personal folders — Desktop, Documents, Downloads, or a folder you explicitly shared. Finder shows the files perfectly; the app reports they are missing, unreadable, or refuses to work with them at all. The same app often works fine with files elsewhere on the disk, which is the clue that this is a location-specific grant problem rather than a general access failure.

macOS manages these locations through the Files and Folders permission: when an app first touches such a folder, the system shows a prompt naming the app and the folder, and your answer (allow or deny) is remembered. A denied prompt, a prompt dismissed without answering, or a grant that was lost after an app update all produce the same present-tense symptom — and the fix path differs slightly for each, which is why this tutorial checks the list state before changing anything.

Likely causes and diagnosis

Three states produce this symptom, and the Files and Folders list tells them apart. The app is absent from the list: it was never granted (the original prompt may have been dismissed), so the fix is to trigger a fresh grant. The app is present but toggled off: you denied it at some point, and re-enabling restores access. And a subtler third: the entry exists for the app you see, but the failing operation runs inside a different binary — a helper tool or command-line companion that is its own privacy subject with its own (missing) entry.

One distinction saves time in terminal-related cases: Terminal (or your terminal app) has its own privacy context, completely separate from the GUI applications it launches. A script run from Terminal inherits Terminal's grants, not the grants of whatever GUI app you might associate with that workflow — so “it works in my editor but not in Terminal” is usually two different subjects being compared (tutorial 20 covers the developer-tooling side in depth).

Step-by-step solution

  1. Reproduce the failure with a specific file: pick one known file in the problem folder and try to open or save it from the failing app. Note the exact error wording — “file not found” (the app cannot see the folder's contents) versus “permission denied” (it sees but cannot touch) both map here, but the record helps later.

    An application showing an error when trying to open a file from the Documents folder
    Step 1 — The specific failing operation reproduced with one known file · img/m-13-s1.png
  2. Open System Settings → Privacy & Security → Files and Folders. Find the failing app's entry (if any) and look at which folders it is granted: the problem folder should appear under it, toggled on.

    The Files and Folders permission list showing an app's per-folder grants
    Step 2 — The Files and Folders list: checking whether the app appears and which folders it holds · img/m-13-s2.png
  3. If the app or its folder grant is missing, trigger a fresh prompt: quit the app, then perform the file operation again — macOS should show the permission dialog for that folder. Accept it. If no prompt appears (some apps suppress re-prompting), use the list's add control to grant the folder directly where the UI offers it.

    The macOS permission dialog asking whether an app may access files in Documents
    Step 3 — The fresh permission prompt for the specific folder, accepted · img/m-13-s3.png
  4. If the entry exists but is toggled off, enable it. On some macOS versions the reliable way to force a re-prompt is to toggle the grant off and back on — the app then asks again, and your fresh “Allow” replaces the old denial.

    Toggling an existing folder grant off and on to force a fresh permission prompt
    Step 4 — The toggle-off/on cycle that regenerates the prompt when a stale denial is blocking · img/m-13-s4.png
  5. Quit and relaunch the application fully, then repeat the exact file operation from step 1. The grant takes effect at launch; a running instance will still see the old state.

    The application relaunched and opening the previously inaccessible file successfully
    Step 5 — The original operation succeeding after a full relaunch · img/m-13-s5.png
  6. If it still fails, check whether the failing component is a helper binary with its own entry (step's logic from tutorial 12 applies), and for terminal-run workflows confirm you are looking at the terminal app's grants rather than the GUI app's.

    The terminal application's own Files and Folders entries, separate from the GUI apps it launches
    Step 6 — Verifying the correct binary's grants when a helper or terminal context is involved · img/m-13-s6.png

Verification and next steps

The fix is verified by the specific file operation from step 1 now succeeding after a full relaunch — ideally tested in both directions (open an existing file and save a new one) since read and write grants can differ. If the app loses this folder access again after updates, tutorial 16 covers the mechanism; if the refusal is on protected system data rather than your personal folders, escalate to Full Disk Access per tutorial 12.

14

Troubleshoot Screen Recording permission problems and blank captures

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

A screen capture that comes out blank or black is almost always the Screen Recording permission missing, stale, or attached to the wrong binary — not a broken camera of pixels. The diagnosis is a short sequence: check the list, restart the app, and test with a capture you can judge at a glance.

Problem and symptoms

Your screen recording or remote-viewing tool shows an empty frame: black where your desktop should be, or a capture that contains only part of the screen. Built-in screenshots may work fine while third-party capture tools fail — which is exactly the pattern that points at the permission layer, because the system's own screenshot utility and third-party apps are evaluated against different grant states.

Screen Recording is one of macOS's most strictly enforced permissions: without an active grant for the capturing application, the system hands it blank content rather than failing loudly. Two version-dependent details matter. On recent macOS releases, capturing system audio is a separate option that appears alongside (or within) the Screen Recording grant — a video with picture but no sound after an update often means the new audio option was left off. And like all privacy grants, the permission attaches to the specific binary doing the capturing, so multi-component tools can need more than one entry.

Likely causes and diagnosis

Work four causes in order. First, no grant at all: the app never appeared in the Screen Recording list (the original prompt was missed or dismissed). Second, a stale grant: the app was updated or reinstalled and its code signature changed, so the system no longer recognizes the old entry — the list may even show an entry that looks correct but belongs to the previous version. Third, the wrong binary: the capture is performed by a helper process with its own privacy identity. Fourth, a missing companion grant: some capture and control tools need Accessibility in addition to Screen Recording to function fully — picture works, control does not.

The test that judges everything is a deliberate capture of something unambiguous: record a short clip of a window with distinctive content (a text document with a known line, a clock) and inspect the result. Blank frame = permission layer; partial frame or missing audio = specific sub-permission; correct frame = the problem was somewhere else entirely, and this tutorial's fixes were not the cause.

Step-by-step solution

  1. Open System Settings → Privacy & Security → Screen Recording. Check for your capture application: absent, present-and-off, or present-and-on. Note whether the UI shows a separate system-audio option on your macOS version.

    The Screen Recording permission list showing capture apps and their toggle states
    Step 1 — The Screen Recording list state before any change · img/m-14-s1.png
  2. Add or enable the grant for the capturing app. If a separate system-audio option exists and your problem includes missing audio, enable that too — picture and audio are independent switches on recent versions.

    The capture application toggled on in Screen Recording with the system audio option also enabled
    Step 2 — The grant (and audio option, where present) applied · img/m-14-s2.png
  3. Quit the capturing application completely and relaunch it. Screen Recording grants are checked at launch; some versions even require a restart of the app after the first grant before captures populate. If the list offered a “quit and reopen” action when you granted, let it do so.

    The capture application being relaunched after the Screen Recording grant was applied
    Step 3 — Full relaunch of the capturing app so the new grant takes effect · img/m-14-s3.png
  4. Run the deliberate test: capture a short clip of a window with known, distinctive content and inspect it. A correct frame confirms the grant worked; a blank frame means the grant is not reaching the binary that actually captures (next step); picture-without-sound points specifically at the audio option.

    A test screen recording of a text document, played back to verify the capture is no longer blank
    Step 4 — The verification capture: a short clip of known content, judged frame by frame · img/m-14-s4.png
  5. If the test is still blank, hunt for the helper binary: check the tool's preferences for a separate capture service or agent, and grant Screen Recording to that component as well. Multi-component remote and streaming tools are the usual case here.

    A helper capture agent added separately to the Screen Recording list
    Step 5 — The helper/capture-service binary granted its own Screen Recording entry · img/m-14-s5.png
  6. If picture works but control features (clicking, typing through the tool) do not, grant Accessibility as well — that is a companion permission, not a Screen Recording sub-setting. Re-test both capture and control after the relaunch.

    The same tool also enabled in the Accessibility list, with capture and control both re-tested
    Step 6 — Companion Accessibility grant added for tools that capture and control · img/m-14-s6.png

Verification and next steps

The fix is verified when the test capture shows correct content (and audio, if that was in scope) across two separate captures — one to rule out a fluke. If the app loses Screen Recording access after each of its updates, tutorial 16 explains the signature-change mechanism and what to expect; if your use case is actually recording specific windows for documentation rather than live capture, the built-in screenshot tools may avoid the permission dance entirely.

15

Fix Accessibility permissions that block automation tools from controlling apps

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

Automation tools that simulate input or drive other applications fail in a characteristically silent way: they launch, look healthy, and simply do nothing. That silence is the Accessibility permission working as designed — and the diagnosis is to grant it deliberately, then prove it with a small action you can watch happen.

Problem and symptoms

An automation, macro, or scripting utility runs without errors but produces no effect: keystrokes never reach the target application, clicks do not register, UI elements cannot be read. The tool's own interface may look perfectly normal — there is often no error message at all, because from the tool's point of view its commands were accepted and then dropped by the system.

This is the Accessibility permission's job. macOS requires explicit user consent before any application can control another one or observe the UI in detail — a protection against exactly this class of tool being used maliciously. The grant lives in System Settings → Privacy & Security → Accessibility, attaches to the specific controlling binary, and takes effect when that binary next launches. Two related permissions are easy to confuse with it: Input Monitoring governs observing keystrokes system-wide (typing analytics, some input enhancements), and Automation governs app-to-app messaging (Apple Events). A tool that both types into apps and reads their windows may need Accessibility plus one of the others.

Likely causes and diagnosis

The usual causes, in order: the grant was never given (the first-launch prompt was dismissed, or the tool was installed before you understood what it needed); the grant was lost to an update (the tool's signature changed and the old entry no longer matches — the list may show a stale-looking entry); or the wrong binary holds the grant (a GUI front end granted, while the actual input-simulating engine is a separate process). Each has the same fix shape — find the right entry for the right binary, enable it, relaunch — but checking the list state first tells you which one you have.

The verification design matters here because of the failure mode: since the symptom is silence, your test must be an action with a visible, unambiguous result. Type a known string into a text field via the tool; click a button whose effect you can see (a dialog appearing, a value changing). A small watched action beats any amount of log-reading, because the permission layer either lets the event through or it does not.

Step-by-step solution

  1. Identify the binary that actually performs the control. For most tools this is the main app; for scripting engines and macro suites it can be a separate engine process or command-line component. The tool's documentation or preferences usually name it.

    An automation tool's preferences pane identifying its background engine component
    Step 1 — Identifying which component of the tool actually simulates input · img/m-15-s1.png
  2. Open Privacy & Security → Accessibility and check for that binary: absent, off, or on. Note any entries that look like they should be the tool but may belong to an older version (stale entries from before an update).

    The Accessibility permission list showing automation tools and their toggle states
    Step 2 — The Accessibility list state, including any stale-looking entries · img/m-15-s2.png
  3. Add or enable the correct entry. If a stale entry from an older version is present and disabled while the current binary is missing, add the current one — do not assume the old entry will “catch up.”

    The current version of the automation tool added and enabled in the Accessibility list
    Step 3 — The grant applied to the current binary, not the stale entry · img/m-15-s3.png
  4. Quit and relaunch the tool completely, then run a small watched test: have it type a known string into a text editor field or click a button with a visible effect. Watch the target application — the event either arrives or it does not.

    A text editor showing the exact string an automation tool typed into it as a verification test
    Step 4 — The watched test: a known string arriving in the target application · img/m-15-s4.png
  5. If input simulation works but reading UI elements still fails (or vice versa), check for the companion permission the specific function needs — Input Monitoring for keystroke observation, Automation for app-to-app commands. Grant it to the same binary and re-test that function separately.

    The Input Monitoring list with the same tool enabled for its keystroke-observing feature
    Step 5 — The companion permission granted when one function still fails after Accessibility · img/m-15-s5.png
  6. If everything works, record which grants this tool needs (Accessibility plus any companions) so its next update — which may invalidate them again — is a two-minute re-grant instead of a fresh diagnosis. Tutorial 16 covers what to expect when updates change signatures.

    A notes file recording the exact permission set an automation tool requires
    Step 6 — The tool's required permission set recorded for future re-grants · img/m-15-s6.png

Verification and next steps

Success is the watched test action completing — input arriving, clicks registering, UI reads returning data — with the grants limited to what the tool actually uses. If the tool stops working after every update with no other change, that is the signature-change pattern from tutorial 16; if the tool needs to control apps it was never granted for individually, check whether its workflow should use the Automation permission's app-pair prompts instead of broad Accessibility access.

16

Investigate an application that keeps asking for the same permission

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

A permission prompt that returns for the same app, over and over, is not a glitch — it is macOS telling you the grant you gave no longer matches the binary asking. The most common reason is an update that changed the app's code signature; the fix is to recognize which entry is stale and replace it, not to keep clicking Allow.

Problem and symptoms

You have already granted a permission — you remember the prompt, you clicked Allow — yet the application asks again. Sometimes on every launch, sometimes after each update, sometimes for what looks like the same thing you approved yesterday. Repeated prompts are annoying in a specific way: they train you to click Allow without reading, which is exactly the habit the permission system exists to prevent.

The mechanism behind most recurring prompts is how macOS identifies an application for privacy purposes: by its code signature (and bundle identity), not just its name. When an app is updated or reinstalled with a new signature, the system no longer sees it as the same subject that holds your old grant — so from its point of view, this is a new app asking for the first time, and the prompt is correct behavior. The settings list can make this confusing: it may show an entry with the app's name that is actually attached to the previous signature, looking present-and-enabled while the current binary sits ungranted.

Likely causes and diagnosis

Four causes cover nearly all recurring-prompt cases. First, signature change after update or reinstall: the classic; the old entry is stale, the new binary needs a fresh grant, and the prompt will keep coming until the list is corrected. Second, multiple binaries in one product: helper tools, agents and command-line companions are separate privacy subjects, each prompting for its own grants — what looks like “the same app asking again” is a different component of it. Third, a denial that the app re-requests by design: some software re-prompts on every launch when its required grant is off, which is legitimate but worth confirming you actually want to allow. Fourth, and least common, corrupted or inconsistent privacy records: the list shows entries that no longer correspond to installed software, or grants that do not take effect; this is where a targeted reset of that one service comes in as a last resort.

Diagnosis starts by reading the prompt itself: it names the requesting application (and sometimes the specific access). Compare that name against the list entries — including checking whether the entry's app icon or location matches what you think it is. Then check the app's update history: if it was updated shortly before the prompts started, cause one is almost certainly in play.

Step-by-step solution

  1. Catch a prompt and read it fully: which application, which access. Then open the relevant Privacy & Security list and compare: does an entry for this app exist, is it on, and does its icon/location match the current installation? Note any entries that look like they belong to an older version.

    A permission prompt next to the settings list showing a stale entry from an older app version
    Step 1 — Prompt and list side by side: spotting the stale entry that no longer matches the current binary · img/m-16-s1.png
  2. If a stale entry exists, remove it from the list (select it and use the list's remove control), then grant the permission fresh to the current application. This replaces the dead grant with a live one and stops the re-prompting at its source.

    Removing the stale entry from a privacy list and adding the current app version in its place
    Step 2 — Stale entry removed, fresh grant applied to the current binary · img/m-16-s2.png
  3. If no stale entry exists but prompts continue, check whether different components of the product are asking: launch each helper or agent the app uses and note which one triggers the prompt. Grant each component its own entry as needed — multi-binary products routinely need several.

    A helper agent process added to a privacy list separately from its parent application
    Step 3 — Each component of the product granted its own entry where required · img/m-16-s3.png
  4. If the app's prompts are by-design re-requests (its grant is off and it asks on every launch), make a deliberate decision: enable the grant if you accept what that access does, or keep declining and check whether the app offers a mode that works without it. Clicking Allow reflexively to silence prompts is how unnecessary grants accumulate.

    The permission prompt being deliberately declined after reviewing what access it requests
    Step 4 — A deliberate allow/decline decision on a by-design re-prompt · img/m-16-s4.png
  5. If entries look corrupted (grants that do not take effect, ghost entries for uninstalled apps), consider a targeted reset of that one privacy service from Terminal: tccutil reset with the specific service name clears all grants for that service system-wide. This is a blunt instrument — it affects every app's grants for that service, so use it only after the list-based fixes above failed, and re-grant what you need afterwards.

    Terminal showing a targeted tccutil reset command for a single privacy service as a last resort
    Step 5 — Targeted service reset in Terminal, used only after list-level fixes were exhausted · img/m-16-s5.png
  6. After whichever fix applied, verify with two clean launches: the app starts, performs its function, and shows no prompt. Two launches matter because some tools only re-check their grants after a full restart cycle.

    The application launching twice without any permission prompt, performing its function normally
    Step 6 — Two clean launches with no prompts: the recurrence fixed at the source · img/m-16-s6.png

Verification and next steps

The fix holds when the app runs its normal function across repeated launches — including one launch after a minor update, which is when signature-change recurrences reveal themselves. If prompts return specifically after updates, that is now a known pattern for this app: keep its required grants recorded (as in tutorials 12 and 15) so re-granting is mechanical. Persistent ghost entries that survive removal point at deeper record corruption, where the targeted reset (step 5) is the documented escape hatch.

17

Fix Terminal commands that receive permission-denied errors

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

Terminal denials come in two flavors with different fixes: a Unix permission error (the file's owner/permission bits say no) and a macOS privacy denial (the system shields that data from your terminal app regardless of who you are). Reading which one you have takes one line of output — and reaching for sudo on the wrong one wastes time while changing nothing.

Problem and symptoms

A command in Terminal fails with a refusal: “Permission denied” or “Operation not permitted.” The file exists — you can see it in Finder, sometimes even open it there — yet the shell will not read, list, or modify it. The frustration is that the same command may work for other paths, which tells you this is a location- or layer-specific block rather than a broken shell.

The two error strings are the diagnostic key. “Permission denied” (Unix EACCES) means the file's permission bits do not allow your user that operation — a classic Unix problem with classic fixes (ownership, chmod, or running as another user where appropriate). “Operation not permitted” (EPERM) on specific protected locations — mail stores, message databases, backup volumes, browser profiles, other apps' containers — means macOS's privacy layer is refusing access for your terminal application. That second class does not care that you own the files or are logged in as an admin: the grant is owed to the terminal app itself, through Full Disk Access.

Likely causes and diagnosis

Run one diagnostic command on the failing path and read its exact wording. If it says “Operation not permitted” and the path is a protected data location, the cause is your terminal app missing Full Disk Access — grant it per tutorial 12 (adding Terminal or iTerm to the FDA list), relaunch the terminal, and re-test. Note an important subtlety: on recent macOS versions, running the command with sudo does not reliably bypass these privacy denials, because the check is attributed to the responsible application (your terminal) rather than merely to the user ID — so sudo is not the fix for this class, and treating it as one produces a false sense of progress.

If instead the error is “Permission denied” on an ordinary file or directory, work the Unix layer: check who owns the path and what its mode bits allow (ls -l), and decide whether the correct fix is changing ownership/permissions for a file you own, using a different user context deliberately, or — if the path is under system protection — recognizing that System Integrity Protection is doing its job and the command should not be pointed there at all. The two layers have different owners (you vs. the system), different tools (chmod/chown vs. privacy settings), and different escalation paths; mixing them up is the most common wasted hour in this problem class.

Terminal — identify which layer is refusing
# Read-only: show ownership and permission bits for the failing path.
ls -ld /path/to/failing/location

# Read-only: try listing it and read the exact error wording.
ls /path/to/failing/location
# "Operation not permitted" on protected data -> privacy layer (Full Disk Access)
# "Permission denied" on ordinary files      -> Unix permission bits

Both commands change nothing. The exact error string is the diagnosis: it tells you which of the two layers to work in.

Step-by-step solution

  1. Reproduce the failure and copy the exact error text. Note the full path involved — protected data locations have recognizable shapes (mail and message stores, backup volumes, browser profile directories), which is itself a clue.

    Terminal showing a command failing with its exact permission error text on a specific path
    Step 1 — The failure reproduced, with the exact error wording captured · img/m-17-s1.png
  2. Run the read-only diagnostics above on that path. The output's error string selects your branch: privacy layer (EPERM on protected data) or Unix layer (EACCES on ordinary files).

    Terminal showing ls -ld output with ownership and mode bits for the failing path
    Step 2 — Ownership and mode bits inspected, error string read · img/m-17-s2.png
  3. If it is the privacy layer: add your terminal application to Full Disk Access (tutorial 12's workflow), quit the terminal completely, relaunch it, and re-run the original command. The grant attaches to the terminal app, so the relaunch is not optional.

    The Terminal application added and enabled in the Full Disk Access list
    Step 3 — Full Disk Access granted to the terminal app itself · img/m-17-s3.png
  4. If it is the Unix layer on a file you own: inspect ownership and mode, then apply the minimal correct fix — for example restoring your user as owner or adjusting the relevant bit. Change only what the diagnosis named; broad permission sweeps on directories you do not fully understand create new problems faster than they solve old ones.

    Terminal showing a targeted chown or chmod command applied to a single owned file
    Step 4 — The minimal Unix fix applied to the specific path, after inspecting its state · img/m-17-s4.png
  5. If the path is under system protection (parts of /System or protected system locations), stop: that refusal is System Integrity Protection working as intended. Do not disable SIP to make a command run — identify what you actually need from that path and use the supported route for it, or accept that the operation is not available on an intact system.

    Terminal showing a refusal on a protected system path, recognized as SIP behavior rather than a fixable error
    Step 5 — Recognizing a SIP-protected path: the correct response is to change the plan, not the protection · img/m-17-s5.png
  6. Re-run the original command as the final test, and record which layer it was and what fixed it. Terminal denial records are short-lived knowledge — the next person (often you, in six months) benefits from knowing this path's refusal was privacy-layer, not Unix.

    The original command now completing successfully after the layer-specific fix
    Step 6 — Original command re-run: success, with the diagnosis recorded · img/m-17-s6.png

Verification and next steps

The fix is verified by the original command completing on the original path, after the layer-appropriate change. If privacy-layer denials persist after a correct FDA grant and terminal relaunch, check whether the command runs inside a wrapper (a shell script launched by another tool) whose responsible process is different — tutorial 20 covers those developer-context cases; if Unix-layer fixes keep regressing, something on the system is resetting ownership or permissions, which deserves its own investigation before more bits are changed.

18

Distinguish Unix file permissions from macOS privacy protections

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

macOS enforces access through two independent layers — the classic Unix permission model and the system's privacy framework — and a denial is only fixable in the layer that produced it. This tutorial builds the mental model: what each layer checks, how to tell which one refused you, and why fixes aimed at the wrong layer change nothing.

Problem and symptoms

You are told “permission denied” (or see its cousin “operation not permitted”) and reach for the first tool that name matches: chmod, sudo, or a privacy settings list. Sometimes it works, sometimes it does not, and the pattern seems random. The non-random explanation is that macOS checks two separate gates in sequence — Unix permissions (who owns the file, what its mode bits allow) and privacy protections (whether this particular application is allowed to touch this category of data) — and passing one gate says nothing about the other.

The practical consequence: a file can be fully readable by your user under Unix rules and still be refused for a specific app by the privacy layer; conversely, an app with every privacy grant it needs can still fail on a file whose Unix bits exclude it. Each layer has its own vocabulary, its own settings surface, and its own correct fix — which is why this tutorial exists before the more specific permission tutorials in this category.

Likely causes and diagnosis

Here is how to read each layer's refusal. The Unix layer speaks in ownership and mode bits: ls -l shows the owner, group, and a string of r/w/x characters for owner, group, and everyone else. A denial from this layer typically reads “Permission denied” and is resolved by correcting ownership (chown) or mode (chmod) — on files you own, with care about what other users or processes depend on the current state.

The two macOS access layers compared: what each checks, how its denials read, where they are configured, and their correct fixes
AspectUnix permission layermacOS privacy layer (TCC)
What it checksFile ownership and mode bits (r/w/x for owner, group, others)Whether the requesting application is granted access to this category of data or capability
Typical error wording“Permission denied”“Operation not permitted” (on protected data and capabilities)
ScopePer file/directory, per user identityPer application (by code signature), per data category or capability
Configured inTerminal: chown / chmod; Finder: Get Info → Sharing & PermissionsSystem Settings → Privacy & Security lists
Does sudo fix it?Often yes (root bypasses most mode-bit checks)Unreliable on recent macOS — the check follows the responsible application, not just the user ID
Correct fix when it refusesInspect and correct ownership/mode for that pathGrant the specific permission to the specific application (tutorials 12–15)

Above both sits a third, non-negotiable layer: System Integrity Protection guards the operating system's own files. Refusals there are not misconfigurations to be worked around — they are the design. This tutorial's consistent position is that SIP should not be disabled as a routine fix for any problem in this library; if a legitimate task appears to require it, the task needs a different route.

Step-by-step solution

  1. When a denial occurs, capture its exact wording and the full path. The string (“Permission denied” vs “Operation not permitted”) plus the path's character (ordinary file vs protected data location) gives you a strong first-layer guess before any command runs.

    Terminal showing a denial with its exact error string and full path noted down
    Step 1 — Error wording and path captured as the layer-selection evidence · img/m-18-s1.png
  2. Inspect the Unix state read-only: ls -ld on the path (and its parent, if the failure is about entering it). Read owner, group, and mode — this tells you whether a Unix-layer explanation is even plausible for your user.

    Terminal ls -ld output showing owner, group and permission bits for the path and its parent
    Step 2 — The Unix layer's state: ownership and mode bits for the path · img/m-18-s2.png
  3. Test your guess with the cheapest discriminating action. If the Unix bits already allow your user read access but the denial stands, the privacy layer is the likely refuser — check the relevant Privacy & Security list for the application doing the accessing (for terminal work, that means the terminal app's Full Disk Access state).

    Privacy and Security settings checked for the accessing application while Unix bits already allow read access
    Step 3 — The privacy layer checked when Unix bits say the denial should not be happening · img/m-18-s3.png
  4. Apply the fix in the layer that refused: correct ownership/mode for a genuine Unix problem (minimal change, on paths you own), or grant the specific privacy permission to the specific application for a privacy problem. Never apply both speculatively — each change should be explainable by the diagnosis.

    A single targeted fix applied in the identified layer, with the reasoning noted beside it
    Step 4 — The layer-specific fix: one change, matched to the layer that refused · img/m-18-s4.png
  5. Re-run the original operation. If it now succeeds, the layer selection was correct and the record is complete. If it still fails, re-read the error — a second, different refusal may have been masked behind the first (both layers can refuse in sequence).

    The original operation re-run after the layer-specific fix, with any new error wording read carefully
    Step 5 — Re-test revealing success, or a second masked refusal from the other layer · img/m-18-s5.png
  6. Keep the two-layer model in your notes for this machine: which paths on this system are privacy-protected data (and which apps hold grants for them), and where Unix ownership has been manually adjusted. That map turns future denials from mysteries into lookups.

    A personal notes document mapping protected paths, granted apps and manual permission changes on this Mac
    Step 6 — The two-layer map for this machine, ready for the next denial · img/m-18-s6.png

Verification and next steps

You have the model working when a new denial can be routed to its layer within a minute — error string, path character, one read-only inspection, one targeted fix. The specific tutorials in this category (12–17) are then just the fix playbooks for each common privacy-layer case, and tutorial 19 applies the same two-layer thinking to external drives, where the Unix layer behaves differently still.

19

Investigate ownership and permission problems on external drives

B · Privacy, permissions & accessAdvancedIntel & Apple Silicon

External drives fail in permission ways that built-in disks never do, because their filesystem decides which of the two access layers even exist. On exFAT volumes there are no Unix permissions at all — so a “permission denied” there points somewhere else entirely; on APFS or HFS+ volumes there is ownership to inspect and, with care, to correct.

Problem and symptoms

An external drive mounts fine and Finder can see its contents — but specific applications cannot write to it, files copied onto it come back unreadable by the tools that need them, or a reformat changed who “owns” everything on the volume. The symptoms vary with which filesystem the drive uses, which is why the first step here is identifying that before touching anything.

Three filesystems dominate external drives, and they behave differently under the access layers. exFAT (the common Windows/Mac shared format) stores no Unix ownership or permission bits at all — every user is effectively allowed what the volume allows, so a denial on exFAT is not a Unix-permission problem; it points at the privacy layer (an app lacking access to that location), a read-only mount, or the drive's own state. APFS and HFS+ are Mac-native formats that do carry ownership and permissions, so files on them have owners and mode bits — and those can drift out of alignment with the user who needs access, especially after reformatting or moving drives between machines.

Likely causes and diagnosis

Diagnosis starts with two read-only facts: the volume's filesystem type and (for Mac-native formats) the ownership of the failing path. The volume information appears in Finder's Get Info for the drive, or from Terminal via disk tools; the ownership appears with ls -ld on the drive root and the specific failing folder. On a Mac-native volume, files owned by a different user than the one doing the work — or owned by root after certain operations — explain write denials directly, and the fix is restoring ownership for your user (with the backup caveat below). On exFAT, skip the ownership question entirely: check whether the volume mounted read-only, whether the accessing app has the privacy grants for that location, and whether another machine or tool left the drive in a locked state.

One more cause deserves its own mention because it is so common: a drive that was reformatted. Erasing a volume to a new filesystem (or even re-erasing the same one) resets ownership of the top-level structure, and anything written afterwards belongs to whoever wrote it — while older expectations about “my” files no longer hold. If the permission trouble started immediately after a reformat or a drive moving between Macs with different user accounts, this is your cause, and the fix is an ownership pass over the affected tree.

Step-by-step solution

  1. Identify the volume's filesystem. Select the drive in Finder and open Get Info (or inspect it from Terminal). Record the format: exFAT, APFS, HFS+ or other — this selects which half of this tutorial applies.

    Finder Get Info window for an external drive showing its filesystem format
    Step 1 — The drive's filesystem identified: the branch point for all later steps · img/m-19-s1.png
  2. On a Mac-native volume, inspect ownership read-only: run ls -ld on the drive root and the specific failing folder. Note who owns each path relative to the user account doing the work — a mismatch here is your Unix-layer cause.

    Terminal showing ls -ld output for an external APFS volume root and a failing folder, with owners visible
    Step 2 — Ownership of the drive root and failing folder, compared against the working user · img/m-19-s2.png
  3. On exFAT (or any format without Unix permissions), redirect the diagnosis: check whether the volume is mounted read-only, whether the accessing application has the privacy grants for that location (tutorials 11–13's lists), and whether the drive shows signs of being locked or in error. No chmod will ever fix an exFAT denial — there are no bits to change.

    A volume's mount state and the accessing app's privacy grants checked for an exFAT drive
    Step 3 — The exFAT branch: mount state and app grants checked instead of permission bits · img/m-19-s3.png
  4. Before changing ownership on a Mac-native volume, make sure the drive's contents are backed up or that you accept the risk: an ownership pass over a large tree is safe in principle but touches every file it visits. The backup is what makes “in principle” into “in practice.”

    A Time Machine or manual backup of the external drive's contents in progress before an ownership change
    Step 4 — The drive backed up before any recursive ownership change · img/m-19-s4.png
  5. Apply the ownership correction to the specific tree that needs it, not the whole volume if a subtree will do: set your user (and standard group) as owner for the affected directory and its contents. Run it on the narrowest path that resolves the failure, and watch the output for errors on individual files.

    Terminal showing a recursive chown command scoped to the specific failing folder on the external volume
    Step 5 — The scoped ownership pass, applied to the narrowest affected tree · img/m-19-s5.png
  6. Re-test the original operation (the app that could not write, the file that could not be read) and confirm the denial is gone. If new denials appear on paths you did not touch, stop widening the fix — re-inspect ownership there specifically before extending any recursive command.

    The previously failing application writing to the external drive successfully after the ownership correction
    Step 6 — Original operation re-tested on the drive: the denial resolved · img/m-19-s6.png

Verification and next steps

The fix is verified when the original failing operation succeeds on the drive, and ownership now reads sensibly (your user owns the working tree) without errors reported during the pass. If denials persist on a Mac-native volume after correct ownership, return to the privacy layer — the accessing app may still lack grants for that location; if the drive itself shows mounting problems or read-only behavior that reappears, the hardware or its controller deserves attention before any more filesystem work.

Safety note Recursive ownership changes visit every file in their path. They are low-risk on a healthy, backed-up volume and genuinely risky on one that is not — which is why step 4's backup precedes step 5's command, without exception.

20

Fix permission failures in IDEs, scripts and AI coding tools

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

Developer tools fail on permissions in a pattern that confuses everyone who thinks of Terminal as the source of truth: an IDE launched from Finder is its own privacy subject, and the scripts it runs are judged by the IDE's grants — not your terminal's. Granting Full Disk Access to Terminal changes nothing for a GUI-launched editor, and this tutorial maps where each grant actually needs to live.

Problem and symptoms

The same script behaves differently depending on how it is launched: run from Terminal it reads your project files fine; run from an IDE's integrated terminal or task runner it fails with access errors. Or an AI coding assistant — a tool that reads, writes and executes code on your behalf — refuses to touch certain folders, or works in some projects and not others, with no obvious pattern in the project locations.

The pattern resolves once you see how macOS attributes access. Every process is evaluated under its responsible application: a script launched from Terminal inherits Terminal's privacy context; a script launched from a GUI application (an IDE, an agent, a build tool with a menu-bar presence) inherits that application's context. Your terminal's Full Disk Access grant is invisible to the IDE — they are different subjects in the privacy system, full stop. So the diagnostic question is never “does this machine have access?” but “which application is responsible for the failing process, and does that application hold the grant?”

Likely causes and diagnosis

Four causes cover most developer-permission failures. First, the GUI app lacks the grant its subprocesses need: the IDE was never added to Full Disk Access (or Files and Folders for specific project locations), so every script it spawns inherits the gap. Second, the tool's helper is a separate subject: many coding tools run a background agent or language server as its own process with its own privacy identity — granting the main window does not cover it. Third, project location matters: projects under Documents, Desktop or Downloads sit behind folder-specific grants, while projects in other locations (a dedicated development folder) may need nothing at all — which explains why “some projects work and others don't” is a location story, not a tool story. Fourth, the tool was updated: signature changes invalidate existing grants (tutorial 16's mechanism), and AI coding tools update frequently.

The discriminating test is simple and decisive: run the failing script from Terminal and from the GUI tool, back to back, on the same file. If Terminal succeeds and the GUI tool fails, the responsible-application gap is confirmed — and the fix is a grant to the GUI tool (and its helper), not to Terminal. If both fail identically, the problem is one level down: the file's Unix permissions or the location's protection class, per tutorials 17 and 18.

Step-by-step solution

  1. Run the discriminating test: execute the failing script (or have the tool perform the failing file operation) from Terminal, then from the GUI application, on the same path. Record which context fails — this single comparison localizes the problem to an application grant or to the file/location itself.

    The same script run from Terminal succeeding and then from an IDE terminal failing on the same file
    Step 1 — Side-by-side test: Terminal context succeeds, GUI tool context fails on the identical path · img/m-20-s1.png
  2. Identify the responsible application precisely. For an IDE, that is the editor app itself; for agent-style tools, check whether a background process or separate agent binary performs the file work (its preferences or process list will show it). This is the subject that needs the grant.

    Activity Monitor showing the background agent process of a coding tool while it performs file operations
    Step 2 — The responsible application (main app or helper agent) identified as the privacy subject · img/m-20-s2.png
  3. Check that application's entries in Privacy & Security: Full Disk Access for broad needs, Files and Folders for specific project locations. Add or enable what is missing — to the main app and to any helper that does its own file work.

    An IDE and its agent helper both enabled in the Full Disk Access list
    Step 3 — Grants applied to the responsible application(s), not just the terminal · img/m-20-s3.png
  4. Quit the tool completely (including background agents — check that no helper process survives) and relaunch it. Privacy grants attach at launch, so a lingering old process will keep behaving with the old context.

    The coding tool fully relaunched with its helper processes freshly started under the new grants
    Step 4 — Full relaunch so all components start under the new grant state · img/m-20-s4.png
  5. Re-run the failing operation from the GUI context. If it now works, also test a project in a different location class (inside Documents vs. in a plain development folder) to learn which of your locations actually depend on grants — that knowledge predicts future surprises.

    The previously failing file operation succeeding inside the IDE after its helper was granted access
    Step 5 — The GUI-context operation succeeding, plus a location-class comparison test · img/m-20-s5.png
  6. Record the tool's required grants (which app, which helper, which permission lists) in your notes. Coding tools update often and signature changes will invalidate these entries (tutorial 16) — with the record, re-granting after an update is a two-minute task instead of a fresh diagnosis.

    A notes file documenting each coding tool's required privacy grants and helper processes
    Step 6 — The grant map recorded, so post-update re-grants are mechanical · img/m-20-s6.png

Verification and next steps

The fix is verified when the GUI-context operation succeeds across two different project locations, with no Terminal context involved. If failures persist after correct grants and full relaunches, check the file layer (Unix permissions on the specific path — tutorial 18) before adding more grants; if a tool's helper cannot be identified or granted cleanly, running that specific workflow from Terminal (where you control the context) is a legitimate interim architecture, not a defeat.

21

Investigate application access problems that appear after an update or reinstall

B · Privacy, permissions & accessIntermediateIntel & Apple Silicon

An app that worked yesterday and cannot access anything today, with the only change being an update or reinstall, is almost always a privacy-identity problem: the system no longer recognizes the new binary as the one that holds your grants. The fix is a deliberate re-grant — but knowing why it happened is what stops you from chasing ghosts in the wrong places.

Problem and symptoms

You updated macOS, or an application updated itself (or was uninstalled and reinstalled), and afterwards one or more apps behave as if their permissions vanished: no access to folders they always read, blank screen captures, automation that no longer acts, prompts returning for things you approved long ago. Nothing else changed — which is precisely the clue.

Two mechanisms produce this timing. The first is signature-based identity: macOS privacy grants attach to an application's code signature and bundle identity. When an app update changes that signature (common after major version bumps, reinstalls from different build sources, or side-loaded updates), the system treats the new binary as a different subject — your old grant still exists in the list, attached to the old identity, while the running app stands ungranted. The second is system-level re-evaluation after macOS upgrades: major upgrades can require you to re-approve third-party system extensions (the Privacy & Security pane shows a pending-approval notice), and some grant states are refreshed in ways that surface as fresh prompts.

Likely causes and diagnosis

Diagnosis is a comparison of three timelines: when the macOS version changed, when each affected app last updated, and when its access broke. Apps whose breakage coincides with their own update are signature-change cases; problems that appeared system-wide at the moment of a major macOS upgrade point at the upgrade's re-evaluation (including any pending system-extension approvals you have not consciously handled). The Privacy & Security lists show the residue either way: entries that look correct but belong to previous identities, and missing entries for current ones.

One important scope note: this mechanism explains lost grants, not lost data or broken functionality. If an app's features are gone beyond its permissions (new requirements, changed behavior), that is a vendor-compatibility problem in the post-update class (tutorial 08) — re-granting will restore access but not new feature support. Keep those two problems separate in your record.

Step-by-step solution

  1. List what broke and when: each affected app, the specific access that failed, and the date it last updated or was reinstalled — alongside the date of any macOS upgrade. The timeline comparison selects your mechanism (per-app signature change vs. system-wide re-evaluation).

    A table matching each affected app's update date against the date its permissions stopped working
    Step 1 — The timeline: app updates, macOS upgrade and permission breakage aligned · img/m-21-s1.png
  2. Check Privacy & Security for pending system-extension approvals first — a major upgrade's notice sits at the top of the pane until you act on it. Approve only extensions from developers you deliberately installed; decline or remove what you do not recognize.

    The pending system extension approval notice in Privacy and Security after a macOS upgrade
    Step 2 — Post-upgrade extension approvals reviewed and consciously decided · img/m-21-s2.png
  3. For each affected app, open the relevant permission list(s) and compare entries against current installations: note stale-looking entries (old identity) and missing ones (current binary ungranted). Do not toggle stale entries on in hope — replace them.

    A permission list showing a stale entry from the previous app version next to the missing current one
    Step 3 — Stale and missing entries identified in the lists · img/m-21-s3.png
  4. Re-grant deliberately: remove stale entries where present, add (or re-trigger prompts for) the current application, granting exactly the permissions it needs per its function — not a blanket sweep of every list. Quit and relaunch each app after its grants change.

    The current version of an app added to its required permission lists after the stale entry was removed
    Step 4 — Clean re-grants: stale entries out, current binary in, apps relaunched · img/m-21-s4.png
  5. Re-test each app's previously failing function specifically — the folder it could not read, the capture that went blank, the automation that stopped acting. Test per app, because a system-wide re-evaluation may have hit different apps in different lists.

    Each affected app's previously failing function re-tested and passing after its re-grant
    Step 5 — Per-app verification of the originally failing functions · img/m-21-s5.png
  6. Record the outcome: which apps lost grants, why (update vs. upgrade), and what was re-granted. This is a known, recurring pattern — your record turns its next occurrence into a checklist run rather than a diagnosis.

    The incident recorded with cause, affected apps and the re-grant list for future reference
    Step 6 — The post-update permission incident documented for next time · img/m-21-s6.png

Verification and next steps

The fix is verified when every affected app performs its previously failing function after a full relaunch, with the lists now holding current (not stale) entries. If an app still fails after a clean re-grant, its problem has moved out of the permission layer — check the vendor-compatibility side (tutorial 08) or the specific-permission workflows (tutorials 12–15). And if you find yourself re-granting after every single minor update of one particular app, note that in your records: some apps' build pipelines change signatures more often than they should, and knowing which ones are yours predicts the maintenance.

22

Audit your granted permissions and reduce unnecessary access safely

B · Privacy, permissions & accessBeginnerIntel & Apple Silicon

Privacy lists grow by accumulation: every prompt answered, every helper added, every app that earned a grant in 2023. An audit is how you convert that history into a deliberate set — removing what no longer exists, questioning what still does, and keeping the machine's access surface as small as your actual workflow needs.

Problem and symptoms

This tutorial has no failure symptom; its trigger is time. Months of normal use leave every Privacy & Security list populated with entries you do not consciously track: apps you have since uninstalled (their entries remain), grants you gave during a one-off troubleshooting session (tutorial 12's FDA entry, still on), and helper processes whose parent app you half-remember. None of this is visible in daily use — which is exactly why the audit exists: to make the invisible surface visible and then shrink it deliberately.

The audit's value is proportional to what the grants mean. A Full Disk Access entry for an uninstalled app is inert; one for a tool you no longer trust, or never fully vetted, is a standing access right to your most sensitive data. The goal is not zero grants — that would break legitimate software — but a set where every entry can be named, its reason stated, and its removal understood: what would stop working if it went off.

Likely causes and diagnosis

The audit walks each list in Privacy & Security and sorts every entry into one of four buckets. Stale: the application no longer exists on this Mac (uninstalled, replaced) — remove it; it serves nothing. Current and needed: an app you actively use that requires this grant for its core function — keep it, and note why if the reason is not obvious. Current but questionable: an app you use, for a grant you cannot immediately justify (why does this utility need Full Disk Access?) — investigate the need before keeping it; many such grants were given during panic troubleshooting and are no longer required. Current and excessive: a broad grant where a narrower one would do (FDA when Files and Folders suffices) — replace with the narrower permission per tutorial 11's mapping.

One safety principle shapes the whole exercise: revoke deliberately, in small batches, and test after each batch. Turning off a grant is immediate and can break an app you use daily without warning — so the audit proceeds list by list, with a relaunch-and-test after each change, rather than as one mass cleanup whose aftermath is impossible to attribute.

Step-by-step solution

  1. Open System Settings → Privacy & Security and work through the lists one at a time, starting with Full Disk Access (the broadest grant) and ending with the narrower ones. For each list, read every entry against what is actually installed on this Mac.

    The Full Disk Access list open during an audit, entries being reviewed one by one
    Step 1 — The audit in progress: Full Disk Access list read entry by entry · img/m-22-s1.png
  2. Remove stale entries: applications that no longer exist on this Mac. Use the list's remove control for each. Stale entries are the audit's free wins — nothing working depends on them, by definition.

    A stale entry for an uninstalled application being removed from a privacy list
    Step 2 — Stale entries removed: apps that no longer exist on the machine · img/m-22-s2.png
  3. For each remaining entry, apply the four-bucket sort. Write down (even just for yourself) the reason each current grant exists — and flag any entry you cannot justify in one sentence. A grant that survives its own justification test earns its place; the rest go to the investigation pile.

    A notes sheet listing each active grant with its stated reason and a flag for unjustified ones
    Step 3 — Each surviving entry justified in one sentence, or flagged for investigation · img/m-22-s3.png
  4. Investigate the flagged grants before touching them: check whether the app still needs the access for its current function (its documentation, or simply testing with the grant off in a spare moment). Where a narrower permission would cover the need, switch to it per tutorial 11's table instead of keeping the broad one.

    A questionable Full Disk Access grant being replaced with a narrower Files and Folders permission
    Step 4 — Questionable grants investigated and narrowed where a smaller permission fits · img/m-22-s4.png
  5. Revoke the decided-upon entries in small batches, relaunching and testing the affected apps after each batch. If something breaks, you know exactly which grant caused it — and can restore just that one — because the batch was small.

    A small batch of grants toggled off with the affected applications being relaunched and tested
    Step 5 — Small-batch revocation with per-batch testing, so any breakage is attributable · img/m-22-s5.png
  6. Finish with the record: the final set of grants, each with its reason, and a date. Schedule the next audit loosely (a new macOS release or a major app change is a natural trigger) — an audited machine stays audited only if the habit repeats.

    The final grant inventory with reasons and the audit date, ready for the next review cycle
    Step 6 — The post-audit inventory: every remaining grant named, reasoned and dated · img/m-22-s6.png

Verification and next steps

The audit is complete when every entry in every list is either removed or has a stated reason you would repeat to someone else, and the daily workflow still functions after each tested batch. If an app resists running without a grant you intended to revoke, that resistance is information — it tells you the grant was load-bearing, so restore it and record why. The standing habit this builds (audit after major changes, keep the reason column current) is worth more than any single cleanup pass.

§05

Category C · 10 tutorials

Storage, APFS & snapshots

Where disk space actually goes on APFS — System Data, local Time Machine snapshots, build artifacts, caches — and how to measure it before touching anything. The order of cleanup matters: verify what a category is before deleting from it, and keep personal data outside the blast radius.

23

Investigate unexpectedly large “System Data” on your startup disk

C · Storage, APFS & snapshotsIntermediateIntel & Apple Silicon

“System Data” is the storage category that swallows disks without explanation — and it is not one thing but a bucket: caches, logs, virtual memory files, local snapshots and miscellaneous system state. Investigating it means finding which member of the bucket grew, with read-only tools first, before anything is deleted.

Problem and symptoms

The Storage view shows your disk filling up, and the category growing is System Data — tens or hundreds of gigabytes that do not correspond to any app or document you recognize. You have not installed anything new; the number simply climbed. The uncomfortable part is that this category has no file browser behind it: macOS summarizes a collection of system-level state rather than showing you the individual files.

Understanding what legitimately lives in this bucket prevents two errors: deleting things that are essential, and missing the thing that is actually reclaimable. Typical members include application and system caches, log files, virtual memory swap and diagnostic data, Time Machine local snapshots (in configurations where they exist), and various system state directories. Most of these are small and normal; the problem case is one member that has grown far beyond its usual size — a runaway cache, an unrotated log, or a snapshot set holding deleted data.

Likely causes and diagnosis

Diagnosis proceeds from the cheapest check to the most specific. First, the built-in suggestions: the Storage view's management screen lists items macOS itself considers reclaimable (large files, certain caches), which handles a fraction of cases with zero manual work. Second, local snapshots: if Time Machine is enabled, local snapshot sets may be holding space that deleted files have not yet released — tutorial 27 covers inspecting them, and their presence explains “I deleted 50 GB and nothing happened.” Third, the user-level state directories: caches and logs under your home Library are visible to read-only size commands and account for most runaway-cache cases; a single application's cache directory growing to many gigabytes is the classic finding.

Terminal — read-only size survey of user state
# Read-only: total size of your caches and logs directories.
du -sh ~/Library/Caches
du -sh ~/Library/Logs

# Read-only: the 20 largest items inside Caches, so you can see
# which application owns the growth before deciding anything.
du -sh ~/Library/Caches/* | sort -rh | head -20

All three commands only read and report sizes — they change nothing. The sorted list is the diagnosis: it names the application whose cache grew.

Step-by-step solution

  1. Record the baseline: open About This Mac → Storage and note the System Data figure, total available space, and (if visible) the last Time Machine backup date. This number is what later steps are measured against.

    The Storage view showing a large System Data segment in the disk usage bar
    Step 1 — The baseline: System Data size and available space recorded · img/m-23-s1.png
  2. Open the Storage management screen (the Manage button) and review macOS's own suggestions. Apply only the ones you understand — large files you recognize, obvious duplicates — and note how much space each suggestion reports as reclaimable.

    The Storage management suggestions screen listing reclaimable items with their sizes
    Step 2 — The built-in suggestions reviewed before any manual work begins · img/m-23-s2.png
  3. Check for local Time Machine snapshots (tutorial 27's commands): if a snapshot set exists and is large, it may be holding the space your deletions did not release. This is a common hidden contributor to System Data figures.

    Terminal listing local Time Machine snapshots with their dates and sizes
    Step 3 — Local snapshot check: whether deleted data is still held by a snapshot set · img/m-23-s3.png
  4. Run the read-only size survey above on your home Library. The sorted Caches list names the growing application; do the same for Logs if caches look normal. A single multi-gigabyte entry is your finding — an application with a runaway cache or log.

    Terminal showing the sorted du output with one application cache far larger than the rest
    Step 4 — The survey result: one cache directory identified as the growth source · img/m-23-s4.png
  5. Act on the finding conservatively. For a runaway application cache: quit that application, then delete only its specific cache subdirectory (caches are regenerable — the app rebuilds what it needs). Never delete parent directories wholesale, and never touch system-level state directories from this workflow.

    Terminal deleting a single application's cache subdirectory after confirming its identity
    Step 5 — The targeted cleanup: one identified cache directory removed, app quit first · img/m-23-s5.png
  6. Re-measure the Storage view after the change (and after a reboot, since some figures settle only on restart). If System Data dropped by roughly what you removed, the diagnosis is confirmed; if it barely moved, the growth lives in a system-level location or snapshot set — return to step 3's findings or escalate with your measurements.

    The Storage view after cleanup showing the reduced System Data segment
    Step 6 — Post-cleanup measurement confirming which change actually released space · img/m-23-s6.png

Verification and next steps

The investigation is complete when the System Data figure is explained — you can name what grew, how much it held, and what you did about it — and available space has recovered accordingly. If the growth returns quickly after cleanup, the source application is still misbehaving: update it, report the leak to its vendor, or monitor the specific directory weekly (the same du command becomes your standing check). Persistent unexplained growth that survives all of the above deserves a fresh look at snapshots and, eventually, Apple Support with your measurements attached.

24

Locate large files and directories with Finder and Terminal

C · Storage, APFS & snapshotsBeginnerIntel & Apple Silicon

Before you can free space, you need to know where it is. Finder's search handles the visible half of your disk; Terminal's size commands handle the rest — including the Library directories Finder hides by default. This tutorial gives both workflows, starting from the safe places and explaining what each command does before you run it.

Problem and symptoms

Your disk is filling up and you need to find the files responsible — not guess at them. The problem with “find big files” as a task is that the biggest consumers are often in locations you do not browse: caches, build artifacts, project support data, old downloads. A workflow that only searches your Documents folder will miss most of the actual answer.

Two tools cover the job with different strengths. Finder can search by size within a chosen scope and shows you the files in their normal context — ideal for personal media, downloads and anything you would recognize on sight. Terminal commands like du (disk usage) report the total size of any directory tree and can rank its contents — ideal for the hidden state directories, which are where multi-gigabyte surprises usually live. Neither tool is destructive by itself when used in read-only mode; the danger enters only at the deletion step, which this tutorial deliberately keeps separate.

Likely causes and diagnosis

The usual large-file suspects on a working Mac cluster predictably: media files you forgot about (video projects, camera originals), downloaded installers and disk images in Downloads, development artifacts (build outputs, container images, simulator data — tutorials 29 and 30 cover those specifically), old Time Machine or backup copies stored locally, and application caches that grew over months. The diagnostic value of a size survey is not just the total — it is the ranking: seeing which directories dominate tells you where to look, and often makes the answer obvious (a 40 GB video project folder does not need further investigation).

One scope warning shapes the whole workflow: scanning the entire disk from its root is slow, produces noise from system locations you should not touch, and on some systems requires elevated privileges that blur the line between inspection and intervention. Start from your home directory — it contains nearly everything a user owns — and only widen scope if the answer is not there.

Step-by-step solution

  1. Start with Finder for the visible layer. In any folder (try Downloads, then your home), use the search field with a size condition — for example searching for files larger than 1 GB — and review what it finds. Sort results by size to see the heaviest items first.

    Finder search results filtered to files over 1 gigabyte, sorted by size
    Step 1 — Finder's size search surfacing the large personal files · img/m-24-s1.png
  2. Move to Terminal for the hidden layer. Open Terminal and run a read-only total on your home directory: du -sh ~ reports its overall size. This is slow on large disks — it walks every file — so let it finish rather than interrupting.

    Terminal running du -sh on the home directory, showing the total size when done
    Step 2 — The home directory's total size measured with a read-only command · img/m-24-s2.png
  3. Rank the top level of your home: du -sh ~/* | sort -rh | head -20 lists the largest items with human-readable sizes, biggest first. This single command usually reveals where the disk went — one or two directories will dominate the list.

    Terminal showing the sorted du ranking of home directory contents with the largest folders on top
    Step 3 — The ranking: which top-level directories actually hold the space · img/m-24-s3.png
  4. Drill into the dominant directory one level at a time, repeating the same pattern on it (for example du -sh ~/Library/* | sort -rh | head -20). Each drill-down narrows the suspect list; stop when you reach a directory whose purpose you understand and whose size you can judge.

    A second-level du ranking inside the dominant directory, narrowing the search further
    Step 4 — Drilling one level deeper until the responsible directory is identified · img/m-24-s4.png
  5. For individual giant files rather than directories, use a find query scoped to your home: it lists files above a size threshold with their paths. Read the results before acting — some “giant files” are projects you need, others are installers you can archive or remove.

    Terminal showing a find command listing individual files over 2 gigabytes with their full paths
    Step 5 — Individual large files listed by path, ready for judgment · img/m-24-s5.png
  6. Record your findings before deleting anything: which directories and files account for the space, and what each one is (project, media, cache, unknown). This record is what separates a deliberate cleanup (tutorial 32) from a panic deletion — and it is the input to deciding what actually goes.

    A notes file listing the large directories found, their sizes and what each one contains
    Step 6 — The findings recorded: sizes, locations and identity of each large item · img/m-24-s6.png

Verification and next steps

The survey is complete when the disk's usage is explained — you can point at specific directories and files whose sizes add up to what Storage shows, with no large unexplained remainder. From there, decide per item using tutorial 32's safety ordering (caches and artifacts before personal data, always), and re-measure after each removal batch so you know exactly what released space. If the survey finds nothing large anywhere but the disk still reports full, the gap lives in snapshots or system state — tutorials 25 and 27 cover that territory.

25

Understand why free-space numbers differ between reports

C · Storage, APFS & snapshotsIntermediateIntel & Apple Silicon

Your Mac can report three different “free space” numbers on the same day — Finder's Storage view, Terminal's disk reports, and the figure an app shows you — and none of them is wrong. Each measures a slightly different slice of an APFS container with shared space, purgeable data and snapshots. This tutorial explains what each number actually counts so the discrepancies stop looking like errors.

Problem and symptoms

You check available space in Storage and see one number; you run a disk command in Terminal and get another; an application warns about low disk based on yet another. The differences can be gigabytes or tens of gigabytes, and the natural conclusion — that something is lying or broken — is wrong. APFS's design makes multiple honest answers possible for the question “how much space is free?”

The root of the discrepancy is that modern Macs do not have simple fixed-size partitions with independent free space. The internal disk is an APFS container — a single pool of physical storage shared by several logical volumes (your main volume, a preboot volume, a recovery volume, and any data or VM volumes). “Free space” at the container level is one fact; what each report shows is how it chooses to present that fact, adjusted for things like purgeable space and snapshot holdings.

Likely causes and diagnosis

Three mechanisms explain most observed differences. First, purgeable space: macOS can mark certain data (notably cloud-offloaded files under optimized storage settings) as reclaimable on demand — it counts as used until the system purges it, but a report that includes purgeable space in “available” will show more free capacity than one that does not. Finder's Storage view has historically presented available space in a way that reflects this flexibility; raw disk commands do not.

Where different free-space figures come from and why they disagree on the same Mac
What you seeWhat it actually countsWhy it differs from the others
Storage view's available figureContainer free space, presented with macOS's view of what is practically usable (purgeable space can flex)Most optimistic number; reflects reclaimable data the system may release under pressure
Terminal disk command output (e.g. df) for a volumeThe APFS container's free blocks, shared across all its volumesRaw and conservative: no purgeable optimism; identical whether you query the main volume or another volume in the same container
An application's low-disk warningWhatever that app's own check measures — often raw free blocks, sometimes with its own safety marginApps differ; some warn earlier than the system's own figures suggest
“Used” that does not match your files' total sizeIncludes local snapshots holding deleted data, system volumes, and APFS overheadYour visible files are only part of what the container holds — tutorial 27 covers the snapshot share

Second, the shared-container effect: because volumes share one pool, querying free space on any volume in the container returns the same container-level number — which surprises people who expect each volume to have its own. Third, snapshots and system volumes hold space that no user file accounts for: local Time Machine snapshots retain deleted data until purged, and the container's other volumes (preboot, recovery) occupy real blocks. A used-space figure that exceeds your files' total size is not an error — it is these non-file residents doing their job.

Step-by-step solution

  1. Collect the disagreeing numbers side by side: the Storage view's available figure, and Terminal's disk report for your startup volume (a standard read-only command such as df -h /). Note how far apart they are and in which direction — a small gap is normal presentation difference; a large one usually has a specific cause.

    The Storage view's available figure next to Terminal's df output, showing the two different numbers
    Step 1 — The two free-space figures captured side by side for comparison · img/m-25-s1.png
  2. Check whether purgeable space is in play. In the Storage view, look for a purgeable indicator (macOS shows reclaimable/purgeable amounts where applicable), and note your storage optimization settings — optimized iCloud storage on this Mac means offloaded files are part of the equation.

    The Storage view showing a purgeable space amount alongside used and available figures
    Step 2 — Purgeable space identified as part of the discrepancy · img/m-25-s2.png
  3. Verify the shared-container behavior: run the same disk command on a different volume in the container (for example your main data volume) and confirm it reports the same free figure. This confirms you are reading one pool, not independent partitions — and rules out “two disks” thinking.

    Terminal showing df run on two different volumes of the same container returning identical free space
    Step 3 — Both volumes reporting the same container-level free space: the shared pool confirmed · img/m-25-s3.png
  4. Account for snapshot-held space. List local Time Machine snapshots (tutorial 27's read-only command): if a substantial set exists, deleted-but-snapshotted data is part of used space and will not appear in any file-size survey you run.

    Terminal listing local snapshots whose held data explains part of the used-space figure
    Step 4 — Local snapshots listed: their held data accounted for in the totals · img/m-25-s4.png
  5. Reconcile the numbers into one explained picture: container free space (the raw fact), plus what each report adds or subtracts (purgeable optimism, snapshot holdings, system volumes). When every gap has a named cause, the “discrepancy” is closed — there was never an error to fix.

    A notes sheet reconciling the different free-space figures with each gap's named cause
    Step 5 — The reconciliation: every difference between reports explained by a mechanism · img/m-25-s5.png
  6. Adopt one canonical number for your own decisions: for “can I install this / start this backup?” questions, use the raw container figure from Terminal — it is the conservative answer that will not disappoint you mid-operation. Treat the Storage view's figure as the system's presentation, not a promise.

    The raw df free-space figure noted as the canonical number for capacity decisions
    Step 6 — The conservative raw figure adopted as the working number for future decisions · img/m-25-s6.png

Verification and next steps

You understand the numbers when a new discrepancy can be explained within minutes using the table above — purgeable, shared container, snapshots, system volumes. If used space still exceeds everything you can account for after that reconciliation, the remaining gap is APFS overhead and system state: real, small relative to the disk, and not something user tools are meant to itemize. For actual capacity problems, the fix path is unchanged: find and reduce what is stored (tutorial 24), or release snapshot-held space (tutorial 27).

26

How APFS containers and shared volumes shape your available disk space

C · Storage, APFS & snapshotsAdvancedIntel & Apple Silicon

APFS does not give each volume its own fixed slice of disk — it gives them all one shared pool, the container. That single design decision explains a family of confusing observations: volumes that report identical free space, disks that fill up “from nowhere,” and capacity behavior that no partition-table mental model can predict. This tutorial builds the correct model.

Problem and symptoms

You have noticed things that do not fit the old mental model of disks as a set of fixed-size partitions: your main volume and another volume on the same disk report the same free space; filling one volume seems to constrain the others; a disk's total capacity does not obviously divide into its volumes' sizes. Each observation is correct — and each is a direct consequence of how APFS allocates space.

The model to replace: in classic partitioning, each partition owns a fixed range of blocks and has its own free space. The APFS model: the physical disk (or RAID set) is organized into one or more containers, each a single pool of blocks; volumes inside a container are logical views that allocate from the pool as needed, with no fixed per-volume ceiling by default. “Free space” therefore exists at the container level, and every volume in the container sees the same number — because they all draw on the same pool.

Likely causes and diagnosis

Three consequences of the shared-pool design matter in practice. First, capacity is collective: when the container fills, every volume in it is affected simultaneously — a large write to any volume can exhaust space that another volume's tools were relying on. Diagnostics should therefore always ask “what is filling the container?” rather than “what is filling this volume?” Second, apparent size flexibility: volumes can grow and shrink within the pool as their content changes, which is why a volume's used size on one day need not predict its behavior on another. Third, snapshots are container residents too: every snapshot of any volume holds blocks in the shared pool, so snapshot activity in one volume consumes capacity available to all.

Terminal — read-only container inspection
# Read-only: print every APFS container, its volumes and their sizes.
diskutil apfs list

This command changes nothing. The output shows the container's total size, each volume's used space, and snapshots — the complete picture of who holds which blocks.

Step-by-step solution

  1. Establish your disk's actual structure: run the read-only container inspection above. Identify the container holding your startup volume, note its total size, and list every volume inside it with its used space.

    Terminal showing diskutil apfs list output: one container with its volumes and used sizes
    Step 1 — The real structure: container, member volumes and each one's used space · img/m-26-s1.png
  2. Verify the shared-pool behavior directly: query free space on two different volumes of the same container (the standard disk command works on any mounted volume) and confirm both return the same figure. This is the model's signature — one pool, many views.

    Free space queried on two volumes of the same container returning identical numbers
    Step 2 — Two volumes, one number: the shared pool demonstrated · img/m-26-s2.png
  3. Map capacity pressure to its source: compare each volume's used space against what you expect it to hold. The volume whose usage exceeds expectations is where the container's pressure originates — and because the pool is shared, that pressure is felt everywhere in the container.

    The per-volume used sizes compared against expectations, isolating the volume driving container pressure
    Step 3 — The over-grown volume identified as the source of shared-pool pressure · img/m-26-s3.png
  4. Check for fixed-size or quota behavior where present: some configurations (certain virtual machine disks, manually created volumes with explicit size limits) do impose per-volume ceilings. The container inspection output distinguishes these; a volume at its ceiling behaves like an old-style partition even inside the shared pool.

    Container inspection output showing a volume with an explicit size limit versus freely growing volumes
    Step 4 — Distinguishing freely shared volumes from any fixed-ceiling ones · img/m-26-s4.png
  5. Factor in snapshots as pool residents: the same inspection lists each volume's snapshots and their sizes. Snapshot-held blocks count against the container's free space just like live data — a snapshot-heavy volume quietly constrains its siblings.

    Snapshot entries in the container inspection, showing how much pool space snapshots hold
    Step 5 — Snapshots counted as shared-pool consumers alongside live data · img/m-26-s5.png
  6. Use the model for decisions: capacity questions (can I add this volume, will this backup fit) are now answered at the container level — total pool size minus everything in it, snapshots included. Record your disk's structure once; future capacity puzzles become arithmetic instead of mystery.

    A one-page record of the container structure with totals used for capacity planning
    Step 6 — The container model recorded and applied to a real capacity decision · img/m-26-s6.png

Verification and next steps

The model is working when you can predict — before running anything — what free-space figure a query on any volume will return, and why. If observed behavior ever contradicts the shared-pool prediction (a volume that cannot grow despite container free space), the explanation is almost always an explicit size limit or quota on that specific volume, visible in the same inspection output. For external drives formatted as APFS with multiple volumes, the identical model applies — which makes this tutorial's mental framework portable across your whole storage setup.

27

Inspect local Time Machine snapshots and when they get reclaimed

C · Storage, APFS & snapshotsIntermediateIntel & Apple Silicon

Local snapshots are invisible backups macOS keeps on your internal disk — and they hold onto deleted files until the system decides to reclaim them. Inspecting them is a two-command job; understanding their lifecycle (when they appear, when they go) is what turns that inspection into an actual explanation for missing free space.

Problem and symptoms

You deleted large files and the disk did not gain the space back. Or System Data grew without any visible cause. In both cases, local snapshots are a prime suspect: they are point-in-time copies of your volume that retain the blocks of deleted data, so from the disk's perspective nothing was actually freed — it is just held by a snapshot.

Local snapshots serve real purposes, which is why they exist at all. Time Machine uses them as its local staging mechanism (snapshots are taken on your internal disk before data is copied to the backup destination), and macOS also creates system-level snapshots around major upgrades so that a failed upgrade can fall back to the previous state. None of this is visible in Finder — the snapshots live in APFS's metadata, not in any browsable folder — which is why they need command-line inspection.

Likely causes and diagnosis

The diagnostic question is always the same: do local snapshots exist right now, and how much do they hold? The read-only listing command answers both — it prints each snapshot's date and identifier for your volume. An empty list means snapshots are not your problem and the space question lies elsewhere (tutorial 24's survey). A populated list means deleted data is retained until those snapshots age out or are purged, and the lifecycle rules determine when that happens.

The lifecycle, in plain terms: local snapshots are created on a schedule while Time Machine is active, and they are reclaimed automatically as space pressure rises or as newer snapshots replace older ones — the system manages this pool without user input. When a Time Machine destination is connected and backing up regularly, local snapshots tend to stay small (their data has been shipped off-disk); when no destination is available, local retention grows to compensate. Pre-upgrade system snapshots follow their own rules: they exist to support rollback and are not part of the routine Time Machine pool.

Terminal — snapshot inspection (read-only first)
# Read-only: list local snapshots of the startup volume.
tmutil listlocalsnapshots /

# Read-only: show whether local snapshots are currently being managed,
# and basic Time Machine status.
tmutil status

Both commands change nothing. An empty first result means no local snapshots exist; otherwise each line is a snapshot holding deleted data until reclaimed.

Step-by-step solution

  1. List the local snapshots for your startup volume with the read-only command above. Note how many exist, their dates, and — if you have deleted large files recently — whether snapshot dates span the deletion window (those are the ones holding your deleted data).

    Terminal showing tmutil listlocalsnapshots output with several dated snapshots
    Step 1 — The local snapshot inventory: dates and identifiers for the startup volume · img/m-27-s1.png
  2. Check Time Machine's state to interpret what you found: is a backup destination configured and connected? A healthy, regularly-backing-up Time Machine setup explains small, transient snapshot pools; a missing or disconnected destination explains growing local retention.

    Time Machine settings showing the backup destination state that explains the snapshot pool size
    Step 2 — Time Machine status checked to interpret the snapshot lifecycle · img/m-27-s2.png
  3. If your goal is simply to understand missing space, you are done: the snapshots explain it, and the reclaim will happen on the system's own schedule (space pressure, destination availability). No action is required — and no manual deletion is needed for a healthy system.

    The space accounting closed: deleted files' blocks held by snapshots, reclamation on schedule
    Step 3 — The explanation complete: snapshot-held space accounted for without intervention · img/m-27-s3.png
  4. If you genuinely need the space now and Time Machine is healthy, the supported lever is connecting your backup destination and letting a backup run — local snapshots whose data has been shipped off-disk become eligible for reclamation. This is the designed path: it releases space while keeping your backup chain intact.

    A Time Machine backup running with the destination connected, after which local snapshots shrink
    Step 4 — A real backup run as the designed way to make local snapshots reclaimable · img/m-27-s4.png
  5. If the snapshot pool is abnormal (very large, no Time Machine destination configured, or system snapshots from a failed upgrade) and space is critical, targeted deletion of specific local snapshots exists as a command — but it is an advanced, deliberate act: you are removing safety data, so understand exactly which snapshot you are deleting and why before running anything.

    A specific snapshot identifier selected for deliberate removal after understanding what it holds
    Step 5 — Deliberate, targeted snapshot removal as the advanced last step, with full understanding · img/m-27-s5.png
  6. Re-measure free space after whichever path you took (and after a reboot if figures have not settled). The released amount should roughly match what the snapshots held — if it does not, other pool residents (tutorial 26's model) deserve a look.

    Free space re-measured after snapshot reclamation, confirming the released amount
    Step 6 — Post-reclamation measurement closing the loop on the space question · img/m-27-s6.png

Verification and next steps

The investigation is complete when you can state: how many local snapshots exist, what they hold, why they are that size (Time Machine state, upgrade activity), and when they will shrink on their own. If space pressure persists with a healthy snapshot pool, the container-level model (tutorial 26) tells you where else to look; if snapshots keep growing abnormally despite a working backup destination, that behavior itself is worth reporting, with your listings attached, to Apple Support.

28

Fix storage that stays nearly full after deleting large files

C · Storage, APFS & snapshotsIntermediateIntel & Apple Silicon

Deleting 50 GB of files and watching available space stay put is not a bug — it is the disk telling you the data still has an owner. Local snapshots, unemptied trash, and cloud-offloaded files that re-download are the three usual owners, and each has a specific check.

Problem and symptoms

You deleted large files — deliberately, in bulk, watching them go — and the Storage view barely moved. The expectation was immediate: delete data, gain space. The reality is that on a modern Mac, “deleted” from your point of view does not always mean “released” from the disk's point of view, and the gap between those two definitions is where this problem lives.

The mental model that fixes the confusion: APFS deletes free a file's blocks only when nothing else references them. A local snapshot taken before your deletion still references those exact blocks — so the disk keeps them, faithfully serving the snapshot, until the snapshot itself is reclaimed. The deletion succeeded; the space just has a second owner you did not know about.

Likely causes and diagnosis

Three causes, in descending order of frequency for this exact symptom. First, local snapshots holding the deleted data: the classic case — check with tutorial 27's read-only listing; if snapshots span your deletion time window, they are your answer. Second, the Trash was not emptied: files moved to Trash (which is what some delete operations do) still occupy their full size until the Trash itself is emptied — a trivial but common oversight in bulk-deletion workflows. Third, purgeable and cloud-offloaded data reappearing: with optimized storage settings, files you deleted locally may be offloaded-to-cloud items that the system re-downloads on access, or purgeable space that flexes back as usage patterns change — the Storage view's figures can move for reasons unrelated to your deletion.

Terminal — verify before and after (read-only)
# Read-only: free space on the startup volume, before any change.
df -h /

# Read-only: local snapshots that may be holding deleted data.
tmutil listlocalsnapshots /

# Read-only: what is actually in the Trash and how big it is.
du -sh ~/.Trash 2>/dev/null

All three commands only report. Run them before acting so you can measure exactly what each fix released afterwards.

Step-by-step solution

  1. Establish the baseline with the read-only commands above: current free space, the snapshot list, and the Trash's actual size. These three numbers frame everything that follows — every later step is judged against them.

    Terminal showing the baseline: free space, snapshot list and Trash size before any change
    Step 1 — The three baseline numbers captured: free space, snapshots, Trash · img/m-28-s1.png
  2. Empty the Trash if it holds anything meaningful. Right-click the Trash in the Dock and choose Empty (or use Finder's menu) — this is the only step that actually deletes data, so confirm you do not need anything in there first.

    The emptying of Trash confirmed after reviewing its contents
    Step 2 — Trash reviewed and emptied, releasing whatever it still held · img/m-28-s2.png
  3. Re-measure free space. If the Trash held the missing gigabytes, the figure has moved and you are done. If not, continue — the snapshots are the next suspect in line.

    Free space re-measured after emptying the Trash, showing whether it was the cause
    Step 3 — Post-Trash measurement: did the figure move? · img/m-28-s3.png
  4. If snapshots span your deletion window (step 1's list), apply tutorial 27's path: with a healthy Time Machine setup, connect the backup destination and let a backup run so the snapshot pool becomes reclaimable. This is the designed mechanism — space returns as the system retires local snapshots whose data now lives off-disk.

    A Time Machine backup completing, after which the local snapshot pool shrinks and space is released
    Step 4 — Backup run completed: the designed path for releasing snapshot-held space · img/m-28-s4.png
  5. Check the purgeable/offloaded angle if the figure still lags expectations: review your storage optimization settings and note whether cloud-offloaded files are in play. Re-downloads of offloaded items can legitimately move the Storage view's numbers without any snapshot involvement.

    Storage optimization settings reviewed for the purgeable and offloaded file angle
    Step 5 — The purgeable/offloaded dimension checked and understood · img/m-28-s5.png
  6. Close the loop with a final measurement and an explanation: state exactly what released how much (Trash: X GB, snapshot reclamation: Y GB) and what remains. If a significant gap still has no owner, widen the investigation to the full container model (tutorial 26) — but in most cases this sequence has already found it.

    The final space accounting: each released amount attributed to its specific cause
    Step 6 — The closed loop: every gigabyte of the recovery attributed to a named cause · img/m-28-s6.png

Verification and next steps

The fix is verified when available space matches your expectations — the deleted data's size has actually been released, with each portion attributed to its cause (Trash emptied, snapshots reclaimed, offloading understood). If space remains stubbornly low with all three causes ruled out, run tutorial 24's full survey: something else on the disk is larger than you think, and the ranking will show it. The standing lesson from this problem class: on APFS, “deleted” and “released” are different events — and the snapshot list is where you check which one actually happened.

29

Clean Xcode DerivedData, build artifacts and simulator data safely

C · Storage, APFS & snapshotsIntermediateIntel & Apple Silicon

A development Mac accumulates Xcode's working data the way a workshop accumulates shavings: individually harmless, collectively gigabytes. The good news is that nearly all of it is regenerable — build outputs, simulator state, intermediate caches — which makes this one of the safest cleanups on this page, provided you inspect sizes first and understand what each deletion costs in rebuild time.

Problem and symptoms

Your disk fills up over weeks of normal development, and a size survey (tutorial 24) keeps pointing at the same place: Xcode's working directories under your home Library. DerivedData alone can reach many gigabytes on active projects; add simulator runtimes and device support files, and a developer's Mac can hold tens of gigabytes of material that no one would consciously keep.

The key fact that makes this cleanup safe: these are generated artifacts, not source data. DerivedData holds compiled intermediates and build products for each project — delete it and the next build regenerates everything, at the cost of a longer first build. Simulator runtimes are downloadable platform images; simulator device state is test data. None of these directories contains your source code or anything irreplaceable — which is precisely why they are the first place to look when a developer's disk fills.

Likely causes and diagnosis

Diagnosis is measurement: find out how much each category actually holds before deciding what to remove. DerivedData keeps one subdirectory per project (named after the project), so long-lived projects with frequent builds accumulate steadily; old projects you no longer touch leave their data behind indefinitely. Simulator runtimes are the other big ticket — each platform runtime is a multi-gigabyte download, and older runtimes remain installed until explicitly removed. Device support files for old iOS versions are smaller but add up across years of development.

The decision rule is simple: remove what you will not rebuild soon, keep what you are actively building. A project you have not opened in months has no reason to keep its DerivedData; the runtime for a platform version you no longer target has no reason to stay installed. The cost of being wrong is bounded and known — a slow rebuild or a re-download — which is exactly why this cleanup ranks near the top of any developer's disk-recovery plan.

Step-by-step solution

  1. Measure first. Check the size of DerivedData and list its per-project subdirectories with sizes, so you can see which projects own the space. Quit Xcode before cleaning — active builds writing to these directories mid-deletion create exactly the mess cleanups are supposed to prevent.

    Terminal showing DerivedData's total size and its per-project subdirectories ranked by size
    Step 1 — The measurement: DerivedData size with its per-project breakdown, Xcode quit · img/m-29-s1.png
  2. Delete DerivedData for projects you are not actively building. You can remove the whole directory (everything regenerates) or just specific project subdirectories (surgical, keeps active projects' build state). Either way, this is safe: source code lives in your repositories, not here.

    Terminal removing DerivedData for inactive projects while keeping the active project's directory
    Step 2 — The targeted removal: stale project build data deleted, active projects untouched · img/m-29-s2.png
  3. Check simulator runtimes: list installed runtimes with the simulator tool and note their sizes. Compare against the platform versions your current projects actually target — anything older than what you build for is a candidate.

    Terminal listing installed simulator runtimes with their identifiers and sizes
    Step 3 — Installed simulator runtimes inventoried against the versions actually in use · img/m-29-s3.png
  4. Delete runtimes you no longer target, using the simulator tool's delete command with the specific runtime identifier. This is a deliberate removal (the runtime re-downloads if ever needed again), so confirm the identifier matches the runtime you mean — the list from step 3 is your reference.

    A specific unused simulator runtime being deleted by its identifier
    Step 4 — The unused runtime removed by explicit identifier · img/m-29-s4.png
  5. Check the remaining Xcode working directories (device support files for old OS versions, intermediate caches) with the same measure-then-decide approach. These are smaller individually but represent years of accumulated development; removing entries for OS versions you no longer support is safe and additive.

    Old device support directories listed with sizes before removing the obsolete ones
    Step 5 — Device support and cache directories measured, obsolete entries removed · img/m-29-s5.png
  6. Re-measure free space and do a sanity build of your active project: the first build will be slower (regenerating what you deleted) — that slowness is the expected cost, and its completion confirms nothing essential was touched. Record how much space the cleanup released.

    The active project completing a clean rebuild after the Xcode artifact cleanup
    Step 6 — The sanity build: slower first compile confirming regeneration works as expected · img/m-29-s6.png

Verification and next steps

The cleanup is verified when free space has recovered by the measured amount, your active project builds successfully from clean state, and no simulator you still use was removed. If disk pressure returns quickly on an active project, that project's build output is simply large — consider cleaning its DerivedData on a schedule (a monthly pass) rather than waiting for the disk to fill again. For non-Xcode development artifacts (package manager caches, container images), tutorial 30 covers the equivalent workflow.

30

Inspect Homebrew caches and package-manager artifacts before cleaning them

C · Storage, APFS & snapshotsIntermediateIntel & Apple Silicon

Package managers are designed to keep copies of what they download — caches exist so reinstalls and downgrades are fast. That design is exactly why a developer's disk quietly fills with bottles, tarballs and old versions nobody thinks about. The cleanup workflow here is inspect-then-prune: measure what each manager holds, use its own pruning tools first, and only touch raw cache directories when the tooling falls short.

Problem and symptoms

Your disk survey (tutorial 24) points at development-related directories you do not recognize in day-to-day use: Homebrew's Cellar and caches under your home or system prefixes, npm's cache, pip's cache, and similar stores for whatever languages and tools you work with. Individually each is doing its job — holding downloaded artifacts for reuse. Collectively they are a multi-gigabyte accumulation of “just in case” copies.

The distinction that governs this whole tutorial: caches are regenerable, installed software is not. A cache entry (a downloaded bottle or tarball) can always be re-fetched — deleting it costs bandwidth and time on the next install. An installed package in Homebrew's Cellar, by contrast, is live software that other tools depend on; removing it is a software decision, not a cleanup one. Confusing the two is how cleanups go wrong, so every step below keeps them separate.

Likely causes and diagnosis

Homebrew's accumulation has three components worth measuring separately. The Cellar holds installed formulae — including old versions of packages that remain after upgrades (the current version is active; older ones are inert weight unless you deliberately keep them for downgrades). The download cache (bottles and tarballs under your home Library's Caches) holds what was fetched for installs. And uninstalled dependencies: packages installed as dependencies of something else, left behind after their parent was removed — Homebrew can list these for you.

Terminal — measure what each manager holds (read-only)
# Read-only: Homebrew's own size and health report.
brew doctor

# Read-only: list packages installed only as leftover dependencies.
brew leaves 2>/dev/null; brew autoremove --dry-run

# Read-only: sizes of the common download caches.
du -sh ~/Library/Caches/Homebrew 2>/dev/null
du -sh ~/.npm 2>/dev/null
du -sh ~/.cache/pip 2>/dev/null

Everything here reports without changing anything — including the dry-run, which shows what autoremove would do. The numbers decide what actually needs cleaning.

Step-by-step solution

  1. Run Homebrew's own inspection: the doctor command reports size and health issues, and the dry-run autoremove shows which leftover dependencies it would remove. Read both before acting — they are the authoritative view of your installation's state.

    Terminal showing brew doctor output with size information and the autoremove dry-run list
    Step 1 — Homebrew's self-report: sizes, health notes and the would-be removals · img/m-30-s1.png
  2. Measure the raw caches (the du commands above) so you know each manager's contribution. A cache that is only a few hundred megabytes is not worth touching; one holding many gigabytes of old downloads is where the real space lives.

    Cache sizes for Homebrew, npm and pip side by side, showing which ones are worth cleaning
    Step 2 — The cache inventory: each manager's download store measured · img/m-30-s2.png
  3. Use Homebrew's own pruning first: its cleanup command removes old versions from the Cellar and stale downloads, with options to control how aggressively it prunes. Prefer the tool's built-in logic over manual deletion — it knows what is safe by construction.

    Terminal running Homebrew's cleanup command removing old versions and stale downloads
    Step 3 — Homebrew's built-in pruning: old Cellar versions and stale downloads removed · img/m-30-s3.png
  4. Remove the leftover dependencies the dry-run identified (running autoremove for real this time). These are packages nothing currently depends on — but read the list first: a package that looks orphaned may be one you use directly and simply forgot to declare as such.

    The autoremove list reviewed, then executed after confirming no actively used package is on it
    Step 4 — Leftover dependencies removed after a deliberate review of the list · img/m-30-s4.png
  5. Clean the other managers' caches with their own tools: npm has cache verification and cleaning commands, pip has a cache purge — each manager's built-in command is safer than deleting its cache directory by hand, because the tool knows its own layout. Run them for whichever caches step 2 showed to be large.

    npm and pip cache maintenance commands running for the managers with large caches
    Step 5 — Each package manager's own cache tooling used for its own store · img/m-30-s5.png
  6. Re-measure everything (the same commands from steps 1–2) and total the released space. Confirm your day-to-day tools still work — a quick build or install test per language covers it. The cleanup is done when the numbers match and nothing you use has broken.

    Post-cleanup measurements showing the released space, with a quick tool sanity check passing
    Step 6 — The after-numbers: released space totaled, tools verified working · img/m-30-s6.png

Verification and next steps

The cleanup is verified when the re-measured caches are at their new (smaller) sizes, installed software still functions, and you have a rough sense of how fast your environment re-accumulates — which tells you the right cadence for repeating this pass (monthly for active developers, quarterly otherwise). If Homebrew's doctor reports problems beyond size (broken links, configuration issues), work those through before further pruning; a healthy installation is what makes the next cleanup safe.

31

Investigate runaway application logs, caches and temporary files

C · Storage, APFS & snapshotsAdvancedIntel & Apple Silicon

Some software does not just use disk — it writes to it continuously, and when a log or cache grows without bound the disk fills on a schedule you can watch. Finding the writer requires separating system logging (which macOS manages for you) from application-level files (which only you can fix), and handling each with the right tool.

Problem and symptoms

The disk fills gradually over days, and re-measuring shows growth continuing in real time — something is actively writing, not merely stored. A size survey points at log files, cache directories or temporary-file locations rather than at media or projects. The signature of this problem class is temporal: the space was fine yesterday, and the gap since then matches a continuous write rate.

macOS keeps two separate logging worlds, and confusing them wastes hours. The system's unified logging (the framework behind Console and the log commands) manages its own storage — it rotates and purges according to system policy, and is not something you clean by hand. Application-level files, by contrast, are ordinary files that each application owns: its own log directory under your home Library's Logs, its own caches, its own temporary working files. When one of these grows without bound — a misconfigured verbosity level, an error loop writing every second, a temp-file leak — the growth is real disk usage with no automatic ceiling, and only the owning application (or you, acting on its behalf) can stop it.

Likely causes and diagnosis

Diagnosis has two parts: identifying which file or directory is growing, and identifying what process is writing it. For the first, repeated size measurements with short intervals between them turn growth into a rate — a directory that gains megabytes per minute while idle has an active writer. For the second, the open-files view (a standard tool showing which processes hold which files open) names the writer directly: a log file held open by a specific process is a solved case, and that process's configuration is where the fix lives.

The likely causes cluster into three patterns. A misconfigured verbosity: debug-level logging left on in production, writing every event to disk. An error loop: a failing operation that retries continuously, each attempt appending an error entry — the log's content itself is the diagnostic here; reading it usually reveals the underlying fault. And a temp-file leak: files created per operation and never cleaned up, accumulating one by one — recognizable as many small files in a temp directory rather than one growing file.

Step-by-step solution

  1. Establish the growth rate: measure the suspect directory's size now, wait a known interval (ten minutes), and measure again. The delta gives you a write rate — fast enough to act on, and a baseline for judging whether later fixes actually stopped the bleeding.

    Two size measurements of the same directory ten minutes apart showing continued growth
    Step 1 — The growth rate measured: the suspect directory's size delta over a known interval · img/m-31-s1.png
  2. Name the writer. Use the standard open-files tool filtered to the growing file or directory to see which process holds it open. The process name is your lead: it tells you whose configuration, log level or bug is responsible.

    The open-files tool output showing the specific process holding the growing log file open
    Step 2 — The writer identified: the exact process keeping the growing file open · img/m-31-s2.png
  3. Read a sample of what is being written. The tail of the log (its most recent lines) usually reveals the pattern: one repeated error (an error loop — fix the underlying fault), high-volume debug entries (verbosity misconfiguration — lower the level), or routine operational data at excessive volume (a design problem to report to the vendor).

    The recent lines of the runaway log showing the repeated pattern that reveals its cause
    Step 3 — The log's content read: the repeated pattern naming the root behavior · img/m-31-s3.png
  4. Fix at the source where possible: adjust the application's logging configuration (verbosity level, log rotation settings it exposes), resolve the underlying error causing a retry loop, or update the application to a version that fixes the leak. Source fixes stop the growth; cleanup alone only delays the next filling.

    The application's logging configuration adjusted to a sustainable verbosity level
    Step 4 — The source fix: configuration or update addressing why the writing runs away · img/m-31-s4.png
  5. Then reclaim what already accumulated. For application-owned files under your home, deleting the specific log or temp directory (with the writer stopped or configured not to rewrite it immediately) is safe — these are the application's working data, not system state. Never apply this logic to system-managed locations: the unified logging store and system directories manage themselves, and hand-deletion there creates more problems than it solves.

    The specific application log directory removed after its writer was stopped and reconfigured
    Step 5 — The accumulated files reclaimed from the application-owned location only · img/m-31-s5.png
  6. Verify the fix held: repeat step 1's measurement over a longer interval (an hour, then a day). A stable size confirms the growth is stopped; continued growth means the source fix did not address the actual writer — return to step 2 with fresh eyes. Record the rate before and after for your incident notes.

    Follow-up measurements over hours showing the directory size now stable after the fix
    Step 6 — The verification: growth stopped, size stable across repeated measurements · img/m-31-s6.png

Verification and next steps

The investigation is complete when the disk's growth has stopped (verified over a meaningful interval), the accumulated data is reclaimed, and the root behavior — misconfiguration, error loop, or leak — is fixed at its source or reported to the vendor with your measurements attached. If the writer turns out to be a system process rather than an application, stop hand-cleaning: that growth pattern belongs in Apple Support's hands, with your rate measurements as evidence. And if you find yourself investigating runaway logs repeatedly across different applications, the standing habit is cheap — a monthly size check on your home Logs directory catches leaks while they are still small.

32

Plan a safe storage cleanup that preserves what matters

C · Storage, APFS & snapshotsBeginnerIntel & Apple Silicon

Disk cleanup fails in one of two ways: it does not free enough space, or it frees space by deleting something irreplaceable. A plan prevents both — by ordering work from least to most risky, verifying after every step, and treating personal data and system directories as categories with different rules entirely.

Problem and symptoms

Your Mac needs space — for an update, a backup, or simply to stop the low-disk warnings — and you face a disk full of things you are not sure you can remove. The temptation in this situation is speed: delete big things until the number goes down. The problem with that approach is that “big” and “safe to delete” are different properties, and a cleanup without an order of operations has no way to keep them separate.

The plan this tutorial provides rests on one organizing principle: every category of disk content has a different cost of being wrong. A cache deleted by mistake costs a re-download or a slower rebuild. A project file deleted by mistake costs the work inside it. A system directory touched by mistake can cost system stability. Order the cleanup by that cost — regenerate first, personal data last (and only with deliberate decisions) — and each step's risk is bounded and known before you take it.

Likely causes and diagnosis

Before ordering anything, the plan needs a map of what is actually on the disk. That is tutorial 24's survey (large directories ranked), plus the category-specific checks from this section: snapshots holding deleted data (tutorial 27), developer artifacts (tutorials 29–30), and the Storage view's own suggestions. The output is a list of specific items with sizes — not “the disk is full,” but “40 GB video project, 12 GB DerivedData, 8 GB old installers, 6 GB local snapshots.” A cleanup that starts from that list is a series of decisions; one that starts from the number is a series of gambles.

Storage content categories and which cleanup actions are appropriate for each, ordered by risk
CategoryExamplesSafe actionRisk if wrong
Regenerable artifactsBuild outputs, DerivedData, package-manager caches, temp files from running appsDelete freely (with the owning tool where it has cleanup commands); they rebuild or re-downloadLow: slower next build, extra bandwidth
Snapshots and system-managed stateLocal Time Machine snapshots, unified logging storeLet the system manage them; release snapshot space via a real backup run (tutorial 27); never hand-delete system storesMedium–high: lost rollback safety; system instability if touched directly
Old software and installersDownloaded app installers, old disk images, uninstalled apps' leftoversDelete after confirming you no longer need the source (re-download is usually possible)Low–medium: re-download time; occasionally a discontinued item you cannot get back
Personal media and documentsVideo projects, photo libraries, archives of workMove to external storage or cloud before deleting; delete only items you have consciously reviewedHigh: irreplaceable work or memories — the category where backups exist precisely for this
System directories and protected pathsAnything under /System, system-level /Library stateDo not delete. These are not cleanup targets on an intact macOS; refusals to modify them are protections working as designedCritical: system instability, unbootable states

Step-by-step solution

  1. Start from a verified backup. Before any deletion — even of “obviously safe” items — confirm a current Time Machine (or equivalent) backup exists and is recent. This one step converts every later mistake from catastrophe into annoyance, and it is the reason the plan works at all.

    Time Machine showing a recent successful backup taken before the cleanup begins
    Step 1 — The precondition: a verified, current backup in place before anything is deleted · img/m-32-s1.png
  2. Build the item list with sizes: run the survey (tutorial 24) and the category checks, and write down each candidate with its size and its category from the table above. This list — not the free-space number — is what the rest of the plan operates on.

    The cleanup candidate list: items with sizes, each tagged with its risk category
    Step 2 — The working list: every candidate item sized and categorized before any action · img/m-32-s2.png
  3. Work the lowest-risk category first: regenerable artifacts. Clean developer caches and build data (tutorials 29–30), clear app caches you have identified, empty installers you no longer need. Re-measure free space after this batch — on many Macs it is already enough.

    The first cleanup batch (caches and artifacts) complete, with free space re-measured
    Step 3 — Batch one done: regenerable items removed, space re-measured · img/m-32-s3.png
  4. Handle snapshot-held space if it is on your list: the designed path (a real backup run with the destination connected) releases it without manual deletion. This step costs time, not judgment — which is exactly why it belongs in the plan rather than improvised later.

    The backup run that allows local snapshots to be reclaimed as part of the cleanup plan
    Step 4 — Snapshot-held space released through the designed backup mechanism · img/m-32-s4.png
  5. Tackle personal data only item by item, with a deliberate decision per item: keep, move to external/cloud storage, or delete. This is the slowest batch and the only one where speed is actively dangerous — an archive of work is not “old files,” it is your history, and its disposition is a decision, not a default.

    A personal media folder being reviewed item by item, with keep/move/delete decisions made deliberately
    Step 5 — The personal-data batch: each item individually decided, nothing bulk-deleted · img/m-32-s5.png
  6. Close out with verification and a record: final free-space measurement against your target, confirmation that applications still function (a quick pass over the ones you touched), and a note of what was removed where. The record matters for next time — it tells you this disk's accumulation pattern and which batches to repeat on a schedule.

    The final measurement hitting the target, with the cleanup record noting what was removed and where
    Step 6 — Target reached and verified: the cleanup documented for its next scheduled run · img/m-32-s6.png

Verification and next steps

The plan is complete when the target space is available, every deletion can be traced to a deliberate decision in your record, and nothing you use daily has broken. Turn the one-off into a habit by scheduling the low-risk batch (step 3's categories) on a regular cadence — monthly for active developers, quarterly otherwise — so the disk never reaches the point where this full plan is needed again. If a future cleanup keeps finding the same category growing back fast, that is a signal to investigate the writer (tutorial 31) rather than re-cleaning forever.

§06

Category D · 12 tutorials

Developer quirks & compatibility

The environment differences that bite developers specifically on macOS: architecture splits between Apple Silicon and Intel, shell and GUI environments, filesystem case behavior, code signing and Gatekeeper, and path assumptions other operating systems never force.

33

Diagnose architecture mismatches between Apple Silicon and Intel in development tools

D · Developer quirks & compatibilityIntermediateIntel & Apple Silicon

A binary built for the wrong CPU architecture fails in ways that look like random corruption: “bad CPU type in executable,” instant crashes, or tools that work on one Mac and not another. The diagnosis is a short set of commands that establish three facts — what your Mac actually is, what the binary actually is, and whether translation is silently involved.

Problem and symptoms

A development tool refuses to run: Terminal reports a bad CPU type error, an application crashes immediately on launch, or a build produces binaries that fail with illegal instruction errors. The confusing part is the inconsistency — the same tool works fine on a colleague's Mac, or worked on this machine before a reinstall, and nothing about your code has changed.

The underlying fact is that macOS runs two CPU architectures side by side. Intel Macs execute x86_64 code natively; Apple Silicon Macs (M-series chips) execute arm64 code natively, with Rosetta 2 translating x86_64 binaries when needed. A universal binary contains both slices and runs anywhere; a single-architecture binary runs natively on matching hardware and only via translation (or not at all) on the other. Most mismatches are not dramatic failures but silent ones: an Intel-only tool running under Rosetta on Apple Silicon, slower and occasionally flaky, which people attribute to “the Mac being slow” rather than to architecture.

Likely causes and diagnosis

Three questions, answered by three read-only commands, diagnose any case. First: what is this Mac? The machine's native architecture is reported directly (and About This Mac shows the chip name — Intel or Apple Silicon). Second: what is this binary? The file command on an executable reports its architecture slices — arm64, x86_64, or both. Third: is translation in play? A process's actual running architecture can differ from the binary's default when Rosetta intervenes, and checking how a tool was launched (native shell vs. translated context) reveals silent mismatches.

Apple Silicon versus Intel development-tool considerations: what differs and what to check
AreaApple Silicon MacsIntel MacsWhat to check
Native architecturearm64x86_64The machine's reported chip / architecture before judging any binary
Translation layerRosetta 2 runs x86_64 binaries (must be installed; some tools prompt for it)Not applicable — Intel code is nativeWhether a tool is running translated (slower, occasionally incompatible with low-level features)
Package manager prefixesHomebrew's default prefix is /opt/homebrew/usr/local is the conventional prefixWhich prefix your tools actually live in — mixing them is tutorial 34's problem
Build outputsDefault builds target arm64; cross-building for Intel requires explicit flags or a universal targetDefault builds target x86_64Whether your build configuration names the architecture explicitly rather than assuming it
Common failure signatures“bad CPU type” on Intel-only binaries without Rosetta; translated tools failing on low-level features“bad CPU type” on arm64-only binaries (no translation path exists from Intel to Apple Silicon)The error direction tells you which side is missing: Intel Macs cannot run arm64 code at all

One asymmetry matters for every diagnosis: translation exists in one direction only. Apple Silicon can run Intel binaries (via Rosetta 2); an Intel Mac cannot run arm64 code, period. So an arm64-only tool on an Intel Mac is a dead end until the vendor ships an Intel or universal build — while the same tool on Apple Silicon will at least run translated.

Terminal — the three diagnostic commands (read-only)
# 1. This Mac's native architecture.
uname -m

# 2. The failing tool's architecture slice(s) — universal shows both.
file /path/to/the/binary

# 3. What architecture a running process is actually using.
ps -o pid,arch,comm -p <pid>

All three commands change nothing. Together they answer: what the machine is, what the binary is, and whether translation is silently in play.

Step-by-step solution

  1. Establish the machine's architecture: run the first command above (and check About This Mac for the chip name). Record it — every later judgment is relative to this fact, and mixed-fleet teams should record it per machine.

    Terminal showing the machine's native architecture next to About This Mac's chip name
    Step 1 — The ground truth: this Mac's architecture recorded before judging any binary · img/m-33-s1.png
  2. Inspect the failing binary's slices with the file command. A universal binary (both architectures listed) should run everywhere — if it fails, the problem is elsewhere. A single-architecture binary must be compared against step 1: matching means native; mismatching means translation or failure.

    The file command output showing a binary's architecture slices, single or universal
    Step 2 — The binary identified: its architecture slice(s) versus the machine's · img/m-33-s2.png
  3. If the binary is Intel-only on an Apple Silicon Mac, check whether Rosetta 2 is installed and functioning — the system usually prompts to install it on first need. After installation, retry the tool: if it now runs (translated), the immediate failure is fixed, with the performance and compatibility caveats of translation noted.

    The Rosetta 2 installation prompt appearing when an Intel-only tool first runs on Apple Silicon
    Step 3 — The translation layer installed where it was missing, and the tool retested · img/m-33-s3.png
  4. If the binary is arm64-only on an Intel Mac, there is no local fix: record the limitation and use the vendor's Intel/universal build if one exists, or run that tool on Apple Silicon hardware. This direction has no translation path — recognizing it quickly saves hours of wrong debugging.

    An arm64-only binary failing on an Intel Mac, with the vendor's universal build checked as the workaround
    Step 4 — The one-way street recognized: no translation from Intel to Apple Silicon · img/m-33-s4.png
  5. For tools that should be native but run translated (or vice versa), check how they are launched: a shell or package-manager context installed for the other architecture can drag its tools along with it. Reinstalling the tool in your native context (the correct prefix and architecture) restores native execution — tutorial 34 covers the Homebrew side of this in depth.

    A process's actual running architecture checked, revealing a tool launched from a translated context
    Step 5 — The launch context checked: native versus translated execution identified · img/m-33-s5.png
  6. Verify the fix with the process-level check: run the tool and confirm its actual running architecture matches your intent (native where you want it, consciously translated where that is all that is possible). Record the machine's setup — architecture, translation state, any tools stuck on the other side — for the team or your future self.

    The process check confirming the tool now runs in its intended native architecture
    Step 6 — Verification: the tool's running architecture matches the intended one · img/m-33-s6.png

Verification and next steps

The mismatch is resolved when the tool runs in its intended architecture, confirmed at the process level rather than assumed from a successful launch. If performance problems persist on translated tools, that is the expected cost of Rosetta — the real fix is a native or universal build from the vendor, which you can now request with precise information (which binary, which slice is missing). For build systems that must produce binaries for both architectures, explicit architecture configuration in the build itself is the durable answer; assumptions about “the Mac's architecture” are what create this whole problem class.

34

Investigate conflicting Homebrew installations and executable paths on Apple Silicon

D · Developer quirks & compatibilityAdvancedApple Silicon focus

Apple Silicon Macs can host two Homebrew installations at once — a native arm64 one in /opt/homebrew and an Intel (Rosetta) one in /usr/local — and when both exist, your shell may be running tools from the wrong one without saying so. The diagnosis is about PATH order and which prefix each tool actually resolves to; the fix is choosing one canonical installation and making everything agree.

Problem and symptoms

Symptoms of a split Homebrew setup are subtle and maddening: a package you installed “doesn't exist” (it exists, in the other prefix), two versions of the same tool visible at once, scripts that work in one shell session and fail in another, or builds linking against libraries from a different installation than the one running them. The common thread is that the system is resolving names to binaries from two independent trees, and which one wins depends on PATH order — which varies by context.

How does this happen? Homebrew on Apple Silicon installs natively into /opt/homebrew by default. But if Homebrew was installed at some point inside a Rosetta (Intel) shell — or an Intel Mac's setup was migrated — a second, x86_64 installation lives in /usr/local, the traditional prefix. Both are fully functional; neither knows about the other. The result is not corruption but ambiguity: every name that exists in both prefixes resolves to whichever PATH entry comes first, and “first” differs between your interactive shell, GUI-launched tools, and scripts with their own environment.

Likely causes and diagnosis

The diagnostic sequence establishes four facts. First: do both installations exist? Check for the presence of each prefix's brew executable — two live installs is the root condition. Second: which one does your shell use? The path of the brew command your shell resolves, plus its reported architecture, tells you which installation your daily commands hit. Third: where do the conflicting tools actually resolve? A which-style lookup with all matches shown reveals when a name exists in both prefixes and which one PATH currently prefers. Fourth: is the ambiguity context-dependent? Compare resolutions between your interactive shell and a GUI-launched terminal or script — differences confirm that PATH order, not installation state, is deciding.

Terminal — map the split (read-only)
# Which brew does this shell resolve, and from which prefix?
which brew
file $(which brew) 2>/dev/null | head -1

# Does the other installation exist too?
ls /opt/homebrew/bin/brew 2>/dev/null
ls /usr/local/bin/brew 2>/dev/null

# Where do a conflicting tool's copies live, and which wins in PATH?
which -a node   # (or any tool present in both prefixes)

Everything here is read-only. The picture that emerges — how many installs exist, which your shell uses, where duplicates resolve — is the complete diagnosis.

Step-by-step solution

  1. Run the mapping commands above and record the results: how many brew installations are live, which one your shell resolves, and for each conflicting tool, where its copies sit and which wins. This map is the plan's input — fix what it shows, not what you assume.

    Terminal showing both Homebrew prefixes present and which one the current shell resolves to
    Step 1 — The split mapped: live installations, shell resolution and duplicate locations · img/m-34-s1.png
  2. Choose the canonical installation. On Apple Silicon, the native arm64 install in /opt/homebrew is the right default — it runs without translation and matches the architecture of modern tooling. The decision should be recorded (for you or your team) because every later step enforces it.

    The decision recorded: the native arm64 Homebrew in /opt/homebrew chosen as canonical
    Step 2 — The canonical prefix decided and documented · img/m-34-s2.png
  3. Make your shell's PATH put the canonical prefix first. Your shell's startup file (the one that configures Homebrew, as its install instructions specified) should place /opt/homebrew/bin ahead of /usr/local/bin — verify the actual order in a fresh shell session rather than trusting the file's text.

    A fresh shell session confirming the canonical prefix now resolves first for brew and shared tool names
    Step 3 — PATH order verified in a fresh shell: the canonical installation wins resolution · img/m-34-s3.png
  4. Reinstall or migrate the packages you actually use into the canonical prefix, working from your list of active tools rather than bulk-copying. Each reinstall in the native context also fixes any architecture mismatch the duplicate carried (a tool installed under the Intel brew may be an Intel build — tutorial 33's territory).

    Packages being reinstalled into the canonical native prefix from a recorded list of active tools
    Step 4 — Active packages migrated to the canonical installation, one deliberate reinstall at a time · img/m-34-s4.png
  5. Decide the non-canonical installation's fate with eyes open. Options: keep it dormant (harmless if PATH never prefers it, but a future footgun), or remove it deliberately after confirming nothing depends on its specific packages. Removal is a software decision — inventory what only it provides before touching anything.

    The dormant second installation inventoried: which packages exist only there, before any removal decision
    Step 5 — The second installation's unique contents inventoried before the keep-or-remove decision · img/m-34-s5.png
  6. Verify end to end: in a fresh shell, confirm brew and each migrated tool resolve to the canonical prefix with the right architecture; then test one GUI-launched context (an IDE terminal) for the same, since its PATH may still carry the old order. Consistency across contexts is the actual success criterion.

    Resolution checks passing in both an interactive shell and a GUI-launched terminal after the fix
    Step 6 — Cross-context verification: every environment now agrees on one installation · img/m-34-s6.png

Verification and next steps

The conflict is resolved when every context you use — interactive shell, GUI terminals, scripts — resolves shared names to the same canonical prefix, with tools running in their intended architecture. If a script still misbehaves after the shell is fixed, its environment is set elsewhere (a launcher, a CI configuration, a cron-like scheduler) and needs the same PATH correction applied at its own level — tutorial 43 covers those non-interactive contexts. The standing rule this setup enforces: one prefix per Mac, named explicitly in every context that matters.

35

Diagnose a command that works in Terminal but fails from a GUI application

D · Developer quirks & compatibilityIntermediateIntel & Apple Silicon

The same command succeeds in Terminal and fails from an app's run button, menu item, or scheduled task — not because the command changed, but because the environment that runs it did. GUI applications on macOS start with a minimal environment that your shell configuration never touches, and most of this failure class is PATH.

Problem and symptoms

You have a command or script that works perfectly when you type it in Terminal — then fails when the same thing is triggered from a GUI application: an editor's task runner, a menu-bar utility, a build tool's start button. The error is usually a missing executable (“command not found”) for something that obviously exists, or a tool that runs but behaves differently (wrong version, missing configuration).

The mechanism is structural, not accidental. When you open Terminal, the shell starts as a login/interactive session and reads your startup files — the ones that add Homebrew's prefix, language toolchains and personal paths to PATH. When a GUI application launches (from Finder, Launchpad, or its own daemon), it inherits the environment of the system's user launch context — which contains no such additions. The app's subprocesses then run with a bare PATH covering standard system locations only. Anything you installed outside those locations is invisible to them, full stop.

Likely causes and diagnosis

Three causes cover most cases, and they nest. First and most common: PATH does not include the tool's directory in the GUI context — the tool lives in a package-manager prefix or personal bin directory that only your shell adds. Second: other environment variables are missing — some tools need more than PATH (a runtime's home variable, a configuration file location, a license server address), and the GUI context supplies none of them. Third: a different version resolves — the bare PATH finds a system-provided or older copy of the tool instead of the one your shell would have chosen, so the command “works” but with surprising behavior.

The diagnostic that separates all three is to capture the environment in both contexts and diff them. In Terminal you can print the full environment; from the GUI side, have the application run the same print (most tools let you run an arbitrary command, or a small wrapper script can log its environment when launched by the app). The differences between the two lists are your complete diagnosis — every missing variable is a candidate cause, ranked by whether the failing tool actually uses it.

Terminal — capture and compare environments
# In Terminal: save the shell's environment for comparison.
env | sort > ~/env-terminal.txt

# Have the GUI app run this (via its command runner or a wrapper script)
# and save the result, then compare:
env | sort > ~/env-gui.txt
diff ~/env-terminal.txt ~/env-gui.txt

All read-only. The diff is the diagnosis: lines present only in the Terminal list are exactly what the GUI context lacks.

Step-by-step solution

  1. Reproduce both behaviors with identical input: run the exact same command/script from Terminal (success) and from the GUI application (failure), capturing the full error text from the GUI side. “command not found” names a PATH problem; a different-version behavior points at resolution order.

    The same command succeeding in Terminal and failing from an app's run button, with both outputs captured
    Step 1 — Both behaviors reproduced side by side with their exact outputs · img/m-35-s1.png
  2. Capture the environment in both contexts using the comparison above. Read the diff top to bottom: note every variable that exists only in the Terminal list, and mark which ones the failing tool plausibly uses (PATH first, then its specific runtime variables).

    The environment diff showing PATH and several variables present only in the Terminal context
    Step 2 — The diff read: exactly which environment entries the GUI context is missing · img/m-35-s2.png
  3. Fix PATH at the application level where it offers configuration: many tools let you set the command's working directory or a custom shell/environment for tasks. Pointing the task at your login shell (which reads your startup files) or supplying the missing path entries directly is the standard fix — no system-wide changes needed.

    An application's task configuration set to run commands through a login shell with the full environment
    Step 3 — The application configured to run its commands in an environment that includes the needed paths · img/m-35-s3.png
  4. Where the application offers no configuration, use full paths: replace bare command names in the failing invocation with absolute paths to the exact binaries you want (the ones your shell resolves). This is explicit and portable across environments — it removes the resolution question entirely for that call.

    A script rewritten to call tools by absolute path, removing dependence on the caller's PATH
    Step 4 — Full paths substituted where app-level configuration is not available · img/m-35-s4.png
  5. For missing non-PATH variables, set them where the invocation happens: export them in the wrapper script that launches the tool, or in the application's environment settings if it has them. Resist system-wide environment hacks (the user-level launch-context setter) as a first resort — they affect every GUI app until reboot and hide the real dependency instead of naming it.

    A wrapper script exporting the specific variables the tool needs before invoking it
    Step 5 — The missing variables supplied at the invocation point, named explicitly · img/m-35-s5.png
  6. Verify in the GUI context specifically — not Terminal: trigger the operation from the application again and confirm it now behaves identically to the shell run. Then test a second launch path (a fresh app launch, or the scheduled-task variant if that was involved) to make sure the fix holds across contexts, not just this session.

    The GUI application's operation now succeeding identically to the Terminal run across fresh launches
    Step 6 — Verification in the original failing context, across multiple launch paths · img/m-35-s6.png

Verification and next steps

The fix is verified when the GUI-context invocation succeeds with output matching the Terminal run, across fresh launches of the application. If different tools keep hitting this same wall in the same app, the durable fix is app-level (its task runner should support login-shell execution or environment files) — report it to the vendor with your diff as evidence. For scheduled and background contexts beyond GUI apps (launch agents, daemons), tutorial 36's environment model applies directly; for IDE-specific variants of this problem, tutorial 20 covers the permission side that often accompanies it.

36

Understand shell startup files: interactive shells, login shells, and GUI environments

D · Developer quirks & compatibilityAdvancedIntel & Apple Silicon

Your configuration exists in several startup files, and which of them run depends on how the shell was started — a distinction that explains why a setting works in one terminal window and not another, and why GUI applications see none of it. This tutorial maps each context to the exact files it reads, so environment questions become lookups instead of guesswork.

Problem and symptoms

The symptoms are the familiar family: a PATH entry or alias that works in your normal Terminal but not in a script; a variable set in one configuration file that some contexts see and others do not; a tool that resolves differently depending on how you launched the shell. Each case has the same root — the executing context read a different subset of your startup files than you assumed.

Since macOS Catalina, the default shell is zsh (older systems may still run bash — nothing in this tutorial assumes one or the other; the file names differ but the model is identical). The model has two axes. The first is login vs. non-login: a login shell (one started at login, or via ssh, or with the login flag) reads the profile files; a non-login shell skips them. The second is interactive vs. non-interactive: an interactive shell (one attached to your keyboard) reads the interactive configuration; scripts and embedded shells do not. Each quadrant of that grid reads a specific file set — and your environment's behavior in any context is exactly the sum of what that context read.

Likely causes and diagnosis

For zsh, the relevant files and their triggers are: the profile file (read by login shells), the interactive configuration file (read by interactive non-login shells), and the system-wide equivalents in /etc that apply to all users. A bash system substitutes its own names (the profile script and the interactive rc file). The classic mistakes follow directly: putting a PATH entry in the interactive file means login shells that are not interactive (some remote sessions, some tool invocations) never see it; putting it in the profile file means interactive non-login shells (a Terminal window opened from an existing session, on some configurations) may miss it. The robust pattern is to put environment facts (PATH, runtime variables) where both contexts converge — or to source one canonical file from both.

Which configuration files each shell context reads, for zsh and bash
Contextzsh readsbash readsTypical examples
Login, interactiveSystem profile files, then the user profile file, then the interactive file (when it is also interactive)System profile files, then the user profile script (and its companions), then the interactive rcA fresh Terminal window on many setups; ssh sessions
Non-login, interactiveThe interactive file onlyThe interactive rc file onlyA shell started inside another shell; some embedded terminals
Non-interactive (scripts)Neither user file by default (only system-level files, and the environment it inherited)Same: script mode reads no user rc/profile by defaultScripts run from cron-like schedulers, build systems, GUI tool subprocesses
GUI application processesNone — they inherit the system's user launch environment directlySame: no shell files at allApps launched from Finder; their spawned subprocesses (tutorial 35)

The diagnostic technique is to ask the running context what it is and what it read. A shell reports whether it is a login shell through its zero-argument value (a leading minus sign indicates login mode), and you can trace exactly which files ran by adding temporary trace lines at the top of each candidate file — then launching each context in turn and seeing which traces fire. Ten minutes of tracing replaces an hour of reasoning about which quadrant you are in.

Step-by-step solution

  1. Identify your actual shell and its startup files: check which shell is configured for your account, list the candidate configuration files that exist in your home directory, and note their contents' themes (which file holds PATH, which holds aliases). You cannot reason about files you have not read.

    Terminal listing the shell configuration files present in the home directory with their sizes
    Step 1 — The inventory: which startup files exist and what each one contains · img/m-36-s1.png
  2. Determine what kind of shell each of your contexts is. In Terminal, check the login-shell indicator (the zero-argument value) and note it for a fresh window, a nested shell, and any remote session you use. This tells you which quadrant each context occupies before any file changes.

    The login-shell indicator checked in several contexts: fresh window, nested shell, remote session
    Step 2 — Each context classified: login vs. non-login, interactive vs. not · img/m-36-s2.png
  3. Trace file execution empirically: add a temporary marker line at the top of each candidate file, then launch each context in turn and observe which markers appear. Remove the markers afterwards. The observed firing order is ground truth — it settles any disagreement between what you assumed and what actually happens.

    Temporary trace lines showing which startup files fire in each tested context
    Step 3 — The empirical trace: exactly which files each context reads, observed not assumed · img/m-36-s3.png
  4. Consolidate environment facts into one canonical file and source it from the contexts that need it. PATH entries, runtime variables and tool configuration belong in a single place (a dedicated environment file), sourced by both the profile and interactive files — so every quadrant that reads either one gets the same facts.

    A single canonical environment file being sourced from both startup files in the editor
    Step 4 — One source of truth: environment facts consolidated and shared by sourcing · img/m-36-s4.png
  5. Handle non-interactive contexts explicitly. Scripts and scheduler-launched jobs read no user files at all — for any script that depends on your environment, either source the canonical file at its top (making the dependency visible in the script itself) or set the needed variables at the scheduling level. Invisible inheritance is how this problem class regenerates.

    A script sourcing the canonical environment file explicitly at its top, making its dependency visible
    Step 5 — Non-interactive contexts made explicit: scripts name their own environment dependencies · img/m-36-s5.png
  6. Verify across the full context matrix: fresh Terminal window, nested shell, script run, and one GUI-launched context — confirming each now sees the environment facts it needs. Record your system's file layout (which file holds what, which contexts read which) so the next environment mystery starts from a map instead of zero.

    The context matrix verified: each environment now resolving the same tool versions and paths
    Step 6 — The matrix check: every context consistent, layout documented for next time · img/m-36-s6.png

Verification and next steps

The setup is sound when the same tool resolves to the same version in every context you use, and a new environment question can be answered by consulting your file map rather than by trial and error. If a GUI context still differs after everything above, that is tutorial 35's territory (the app's own launch environment), not a shell-file problem — the two tutorials are complementary halves of the same wall. And if you maintain machines with different shells or mixed user accounts, keep the canonical-file pattern per machine: it is portable across zsh and bash alike.

37

Diagnose case-sensitivity problems between macOS filesystems and other environments

D · Developer quirks & compatibilityIntermediateIntel & Apple Silicon

macOS's default filesystem treats names that differ only in letter case as the same file — while Linux, CI servers and many build tools do not. The result is a classic failure family: code that works on your Mac and breaks on the server (or git conflicts that appear from nowhere). The diagnosis is a two-second test; the fixes range from renaming to understanding why reformatting is usually the wrong answer.

Problem and symptoms

The signature cases: a build fails on Linux or in CI with “file not found” for a file that exists on your Mac (the import uses different casing than the filename); git reports conflicts or shows files as both added and deleted when two branches create names that differ only in case; or a tool renames a file and your working tree silently holds one of the two names. On your Mac, everything looked consistent — because the filesystem was hiding the inconsistency for you.

The mechanism: APFS volumes can be formatted either case-insensitive (the default for macOS installations) or case-sensitive. On a case-insensitive volume, Foo.js and foo.js are the same name — the system stores one file under whichever casing was used first, and lookups succeed regardless of the casing you type. On a case-sensitive volume (and on typical Linux filesystems), they are two distinct names. Code that depends on exact casing therefore behaves differently across these environments even though it is byte-for-byte identical.

Likely causes and diagnosis

Diagnosis has two parts. First, establish your volume's mode: a quick empirical test (create one name, then try creating its case-variant — success means case-sensitive, an “already exists” error means case-insensitive) settles it in seconds without any system query. Second, find the actual conflicts in your projects: search the repository for file pairs that differ only in case (a scripted comparison of the file list against its lowercased form reveals them), and check build failures' missing-file names against what actually exists on disk — a casing mismatch between the failing reference and the real filename is your finding.

The fix space is wider than people expect, and reformatting is usually not in it. Most cases are fixed by renaming: making the on-disk name match the casing that imports and references use (consistently, across the repository). Some projects legitimately need case-sensitive behavior — for those, macOS can format volumes case-sensitively, but a reformat of your startup disk is a destructive operation (full backup required, everything erased) that most situations do not justify; a dedicated case-sensitive volume or container for the affected project is the proportionate version. And for teams, the durable fix is a CI check that catches casing conflicts before they reach anyone's machine.

Step-by-step solution

  1. Determine your working volume's case mode with the empirical test: in an empty folder, create a file named Test.txt, then attempt to create test.txt. If the second creation succeeds (two files exist), the volume is case-sensitive; if it reports the name already exists, it is case-insensitive — the macOS default.

    The two-file test in Terminal: attempting to create a case-variant name and reading the result
    Step 1 — The volume's case mode established by direct test · img/m-37-s1.png
  2. If your volume is case-insensitive (the usual case), find the conflicts in your repositories: script a comparison of each project's file list against its own lowercased form — any name whose lowercased twin also exists (differently) is a conflict waiting to happen on case-sensitive systems.

    A script listing file pairs in a repository that differ only in letter case
    Step 2 — The conflict scan: name pairs in the project that a case-sensitive system would treat as distinct · img/m-37-s2.png
  3. For each conflict, decide the canonical casing — matching what the code's imports and references actually use — and rename the file to it. In git, a case-only rename needs explicit handling (the two-step rename via an intermediate name avoids the filesystem collapsing the change); commit the rename so every clone agrees.

    A case-only file rename committed to git using the two-step intermediate-name method
    Step 3 — The conflicting name renamed to its canonical casing and committed · img/m-37-s3.png
  4. Verify on a case-sensitive environment before declaring it fixed: run the build or the failing test in a container, CI pipeline, or Linux machine — the environments where this class of bug actually lives. Success there (not just on the Mac) is the real verification.

    The same project building successfully in a case-sensitive CI environment after the rename
    Step 4 — Verification where it matters: the case-sensitive environment now builds clean · img/m-37-s4.png
  5. Add a standing check so conflicts cannot return: a small CI step (or pre-commit hook) that scans for case-duplicate names and fails the build when it finds them. This converts a per-developer discipline into a repository guarantee — the only fix that survives team turnover.

    A CI pipeline step scanning for case-duplicate filenames and reporting its result
    Step 5 — The standing check in place: case conflicts caught at commit or build time · img/m-37-s5.png
  6. Only if the project genuinely requires case-sensitive development on the Mac itself, evaluate the dedicated-volume option: a separate case-sensitive APFS volume for that work (or a container with a case-sensitive filesystem), not a reformat of the startup disk. Weigh the friction against the actual need — most projects are fully served by canonical naming plus the CI check.

    A dedicated case-sensitive volume created for a project that genuinely needs it, startup disk untouched
    Step 6 — The proportionate option: a dedicated case-sensitive workspace when the project truly requires it · img/m-37-s6.png

Verification and next steps

The problem is closed when the repository contains no case-duplicate names, the build passes in a case-sensitive environment, and the standing check prevents recurrence. If git behavior still looks odd after renames (files appearing as both deleted and added), the local working tree may hold a stale casing — a clean re-clone of the repository resolves that residue. The general lesson transfers to every cross-platform project: your filesystem's name semantics are a feature you depend on, and dependencies should be made explicit in CI rather than assumed away.

38

Troubleshoot file-watcher limits and missed filesystem events in large projects

D · Developer quirks & compatibilityAdvancedIntel & Apple Silicon

A dev server that stops noticing your edits is usually not broken — it is overwhelmed. File-watching on macOS has real limits: per-process file descriptor ceilings, event coalescing under load, and watchers that silently give up when a project's directory count grows past what they can track. The diagnosis separates “the tool gave up” from “the event never happened” — and the fixes are about scope, not magic.

Problem and symptoms

The symptom has a characteristic shape: hot reload or watch-mode builds work fine for a while, then start missing changes — sometimes specific files, sometimes everything after a certain time, sometimes only after the project grows past a certain size. Re-saving a file or restarting the tool temporarily restores behavior, which is the clue that this is a capacity problem, not a logic bug.

macOS filesystem watching works through kernel facilities (the kqueue family and the higher-level event stream built on it) with two properties that matter at scale. First, every watched directory consumes a file descriptor in the watching process — and processes have finite descriptor limits, so a watcher tracking tens of thousands of directories can exhaust its budget and stop registering new watches. Second, under heavy filesystem activity the kernel coalesces events: rapid successive changes to the same path may arrive as one notification, and extremely busy periods can drop or delay events that user-level code expects. Both properties are normal system behavior — but from a developer's chair they look like “my tool stopped working.”

Likely causes and diagnosis

Three causes, distinguishable by their signatures. First, descriptor exhaustion: the watcher's process runs out of file descriptors as the project (or node_modules-style dependency trees) grows — the signature is gradual degradation with project size, and the process's open-descriptor count near its limit. Second, event coalescing under load: bursts of simultaneous changes (a build writing thousands of files while the watcher runs) overwhelm the event stream — the signature is missed changes clustered around heavy write activity, with quiet edits still being caught. Third, watcher scope misconfiguration: the tool watches more than it needs (entire home directories, mounted volumes, symlinks that fan out to huge trees), multiplying its descriptor cost without adding useful coverage.

Terminal — inspect the watcher's resource state (read-only)
# Read-only: how many file descriptors is the watching process using?
lsof -p <pid> | wc -l

# Read-only: what is that process's limit?
ulimit -n

# Read-only: how large is the tree it is actually watching?
find /path/to/project -type d | wc -l

All read-only. Usage near the limit, or a watched tree with more directories than the tool can reasonably track, points at capacity — not at your code.

Step-by-step solution

  1. Characterize the failure precisely: which changes are missed (all files? specific directories? only during builds?), when it started (project growth? new tool version? new hardware?), and whether restarts help temporarily. The pattern selects the cause — gradual size-correlated degradation suggests descriptors; burst-clustered misses suggest coalescing.

    A notes sheet characterizing the missed-change pattern: which files, when, and what restores behavior
    Step 1 — The failure characterized: pattern, timing and temporary fixes recorded · img/m-38-s1.png
  2. Measure the watcher's resource state with the commands above while it is running: its open-descriptor count against its limit, and the directory count of the tree it watches. Usage within 80–90% of the limit is your exhaustion diagnosis; a watched tree an order of magnitude larger than the project's source is your scope diagnosis.

    Terminal showing the watcher process's descriptor usage near its limit and the watched tree's directory count
    Step 2 — The resource measurement: descriptor pressure and watched-tree size quantified · img/m-38-s2.png
  3. Narrow the watch scope to what the tool actually needs. Exclude dependency trees, build output directories and vendored code from watching (every build tool has ignore patterns for exactly this) — source files are usually a small fraction of a project's total directory count, and the exclusion list is where most of the capacity comes back.

    A build tool's ignore configuration excluding dependency and build directories from file watching
    Step 3 — The watch scope narrowed: dependencies and build output excluded from monitoring · img/m-38-s3.png
  4. If descriptor pressure persists with a reasonable scope, raise the process's limit where the platform allows it (the per-user or per-process soft limit can often be increased for your own processes) and restart the tool under the new limit. This is a legitimate capacity adjustment — not a system-wide change — and it should be recorded so future sessions start with it.

    The file descriptor soft limit raised for the development session, then the watcher restarted under it
    Step 4 — The process-level limit raised deliberately and documented · img/m-38-s4.png
  5. For coalescing-under-load signatures, reduce concurrent filesystem churn: sequence operations that mass-write (builds, installs) so they do not run while watching is critical, or configure the tool's debounce window to tolerate bursty writes. The goal is not to stop the churn but to make the watcher's expectations match reality — it will coalesce under load, by design.

    A tool's debounce configuration adjusted so bursty build writes do not produce false missed-change reports
    Step 5 — The debounce tuned: the watcher's expectations matched to the project's write patterns · img/m-38-s5.png
  6. Verify with a sustained test: make edits across the project over a working session (including during a build) and confirm changes are picked up. If misses persist after scope, limits and debounce are all correct, the tool's watcher implementation is the suspect — report it with your measurements (descriptor counts, tree sizes, patterns), which now distinguish this class of problem from ordinary bugs.

    A sustained editing session with builds running, all changes picked up by the watcher after the fixes
    Step 6 — The sustained verification: edits and builds coexisting without missed events · img/m-38-s6.png

Verification and next steps

The fix is verified when a full working session — edits, saves, builds — produces no missed changes, with the watcher's descriptor usage comfortably below its limit. If the project keeps growing past what any tuning can cover, the architectural answer is the tool's own: incremental watch strategies, directory-level rather than file-level watching, or explicit rebuild commands for the parts that genuinely cannot be watched reliably. Record your machine's working configuration (scope exclusions, limits, debounce) per project — this class of problem has no universal setting, only a per-project equilibrium.

39

Investigate Gatekeeper, quarantine and apps that macOS refuses to open

D · Developer quirks & compatibilityIntermediateIntel & Apple Silicon

“The application cannot be opened because the developer cannot be verified” is not a broken app — it is Gatekeeper doing its job on a file that arrived with a quarantine mark. Understanding where the mark comes from, what the legitimate bypass paths are, and why disabling Gatekeeper system-wide is the wrong fix turns this from a scuffle into a five-minute diagnosis.

Problem and symptoms

You download or copy an application, double-click it, and macOS refuses: the app cannot be opened because the developer cannot be verified, or (on some versions) a warning that the app was downloaded from the internet. The app is one you deliberately obtained — a vendor build, an open-source release, a tool from a colleague — so the refusal feels arbitrary. It is not; it is the system applying a rule to a mark on the file, and both the mark and the rule are inspectable.

The mechanism has two parts. The quarantine attribute is a flag macOS attaches to files that arrive from outside the machine — browsers set it on downloads, and it travels with the file through copies in some paths. It says: “this content came from the network; evaluate it before first use.” Gatekeeper is the evaluator: when a quarantined app launches for the first time, the system checks its provenance — App Store origin, or a Developer ID signature with notarization on file — and blocks what it cannot verify. Apps you build locally, or that arrived without the mark, never trigger this path, which is why the same binary can open fine on one Mac and be refused on another.

Likely causes and diagnosis

The diagnostic sequence reads the file's actual state rather than guessing. First: does the quarantine mark exist? The extended-attributes command shows it directly (an attribute named com.apple.quarantine). Second: why is Gatekeeper refusing? The assessment command reports the specific reason — unsigned, signed but not notarized, or notarization lookup failing. Third: is this app one you should trust at all? This is a security decision that precedes any technical fix: a Developer ID signature from a known vendor is one situation; an unsigned binary from an unknown source is another, and no bypass in this tutorial changes what the second one should be.

Terminal — inspect provenance (read-only)
# Read-only: does the app carry the quarantine attribute?
xattr -l /path/to/App.app

# Read-only: what does Gatekeeper's assessment say, and why?
spctl --assess --type execute -v /path/to/App.app

# Read-only: is the app signed at all, and by whom?
codesign -dv /path/to/App.app 2>&1 | head

All three commands only inspect. Together they answer: is it quarantined, what does the system think of it, and who signed it — the complete provenance picture.

Step-by-step solution

  1. Run the three inspection commands on the refused app. Record what they show: quarantine present or absent, the assessment's stated reason, and the signing identity (if any). This record is both the diagnosis and the security decision's input.

    Terminal showing the quarantine attribute, Gatekeeper assessment result and signing info for a refused app
    Step 1 — The provenance inspected: quarantine mark, assessment reason and signature identity · img/m-39-s1.png
  2. Make the trust decision explicitly. A known vendor's Developer ID-signed app that fails only on notarization lookup (a transient Apple-side issue) is one category; an unsigned app of unknown origin is another. The technical steps below are for apps you have decided to run — for anything else, the correct action is not to open it at all.

    The trust decision documented: which vendor signed the app and why it is being run despite the block
    Step 2 — The deliberate trust decision recorded before any bypass step · img/m-39-s2.png
  3. Use the designed first-run path: right-click (or Control-click) the app and choose Open, then confirm in the dialog that appears. This records your explicit decision for this specific app without changing system-wide policy — it is the mechanism macOS provides precisely for “I have checked this one myself.”

    The right-click Open menu and its confirmation dialog for a Developer ID app
    Step 3 — The per-app explicit approval: right-click Open, confirmed once · img/m-39-s3.png
  4. If the assessment showed a signing problem (the binary's signature is broken or missing when it should have one), the fix is upstream: obtain the app again from its source. A broken signature often means the file was modified in transit or after download — re-downloading from the vendor, and verifying the download if the vendor provides checksums, addresses the real cause.

    A fresh download of the same app with its signature verified intact before opening
    Step 4 — The broken-signature case: re-downloading and verifying integrity from the source · img/m-39-s4.png
  5. For apps you build or distribute yourself (tutorial 40 covers the signing side), the durable fix is proper signing — ad-hoc for local use, Developer ID for distribution — rather than any quarantine workaround. Workarounds on your own builds mask the configuration problem instead of solving it.

    A locally built app after being signed, opening without any Gatekeeper refusal
    Step 5 — The self-built case: signing applied so the app opens cleanly by design · img/m-39-s5.png
  6. Verify the outcome and leave the system's policy untouched: the app opens, the refusal is gone for that app specifically, and Gatekeeper's system-wide settings remain at their defaults. If you find yourself needing per-app bypasses constantly, that pattern deserves investigation (a misconfigured build pipeline) rather than accumulation.

    The app running normally with Gatekeeper's system-wide policy left at its default state
    Step 6 — Verification: the specific app opens, system policy unchanged · img/m-39-s6.png

Verification and next steps

The situation is resolved when the app launches, its provenance is understood (and recorded), and no system-wide security setting was changed to achieve it. If a trusted vendor's app keeps failing assessment after clean re-downloads, the issue may be on Apple's notarization side or in the vendor's signing — report it with your spctl output, which names the exact failure. And if you are the one building these apps, tutorial 40 is the next step: getting the signing configuration right means this refusal never appears for your software in the first place.

Safety note Disabling Gatekeeper system-wide (or removing quarantine attributes wholesale) is not a routine fix in this library. Those settings protect every future download on the machine; the per-app paths above achieve the same outcome for the specific app you have vetted, without lowering the floor for everything else.

40

Diagnose code-signing and notarization errors in locally built applications

D · Developer quirks & compatibilityAdvancedIntel & Apple Silicon

Locally built apps fail in signature-specific ways: the “app is damaged” error after a rename or modification, verification failures that name nested binaries, and Gatekeeper rejections that are really signing-configuration problems. The diagnosis follows one rule — a code signature covers the app's contents as a sealed whole — and the fixes range from ad-hoc signing for local work to proper notarization for anything that leaves the machine.

Problem and symptoms

Your home-built application refuses to run, or runs on your Mac but is refused everywhere else. The specific errors carry their own diagnostics: “app is damaged and should be moved to the Trash” (a signature that no longer matches its contents), verification output naming a nested framework or binary as invalid, and Gatekeeper assessment failures for unsigned or ad-hoc-signed software. Each points at a different place in the signing pipeline.

The rule that explains all of them: a code signature is a seal over the signed content — the executable, its embedded frameworks, resources, everything inside the bundle (or the binary, for command-line tools). Modify any covered byte after signing and the seal breaks: the system detects the mismatch and refuses the app. Renaming certain internal paths, repacking the bundle through tools that alter contents, or patching a binary post-sign all produce the same broken-seal state. This is why “it worked before I changed X” is the most common history behind signature errors on local builds.

Likely causes and diagnosis

Four causes, each with a distinct diagnostic fingerprint. First, unsigned or ad-hoc-signed software meeting Gatekeeper's distribution expectations: locally built apps carry no Developer ID, so any context that expects verified provenance (another Mac, an archive download, a CI artifact) rejects them — the fix is signing appropriate to the app's destination. Second, a broken seal from post-sign modification: the damaged-app error; verification output will name the mismatched component, and the history (what touched the bundle since signing) identifies the culprit. Third, incomplete signing depth: the main executable signed but nested code (helper tools, embedded frameworks) left unsigned or signed inconsistently — verification with deep checking exposes exactly which components fail. Fourth, notarization gaps for distribution: Developer ID signing without a completed notarization ticket still fails Gatekeeper on other machines; the ticket's status is checkable and names the specific rejection reason when it failed.

Terminal — verify signatures (read-only)
# Read-only: does the signature match the current contents?
codesign --verify --deep --strict -v /path/to/App.app

# Read-only: what identity (if any) signed it?
codesign -dv /path/to/App.app 2>&1 | head

# Read-only: what does Gatekeeper's assessment say?
spctl --assess --type execute -v /path/to/App.app

All read-only. The first command is the seal test: silence means valid, and any output names the component that broke it.

Step-by-step solution

  1. Run the verification trio on the failing app and read the results precisely. The seal test's output (if any) names the broken component; the identity query shows what signature exists at all; the assessment states Gatekeeper's verdict. Together they place the failure in one of the four cause categories.

    Terminal showing codesign verification output naming the specific component whose signature failed
    Step 1 — The failure placed: verification output read component by component · img/m-40-s1.png
  2. If the seal is broken (damaged-app error), establish what modified the bundle after signing: build steps that repack, rename or patch post-sign, archive tools that alter contents in transit, or manual edits. The fix is to restore the pipeline's order — sign last, over final contents — not to chase the mismatched bytes.

    A build script reordered so the signing step runs after all content modifications are complete
    Step 2 — The pipeline corrected: signing moved to the final step over finished contents · img/m-40-s2.png
  3. For local-use apps, apply ad-hoc signing: sign the bundle with the ad-hoc identity so it carries a valid seal without any developer account. This satisfies local execution on your own Mac (and explains why unsigned local builds should at minimum be ad-hoc signed — the seal is what local verification checks).

    An ad-hoc signing command applied to a locally built app, followed by a passing verification
    Step 3 — Ad-hoc signing for local use: the seal restored without any account · img/m-40-s3.png
  4. For anything distributed beyond your own machine, sign with a Developer ID identity and complete notarization: submit the signed app for Apple's notarization service and staple the resulting ticket to the bundle. The staple is what lets other Macs verify provenance offline — without it, distribution hits Gatekeeper walls regardless of correct signing.

    The notarization submission and ticket stapling steps for a Developer ID signed app
    Step 4 — The distribution path: Developer ID signature plus completed, stapled notarization · img/m-40-s4.png
  5. Check signing depth explicitly: verify with deep checking that every nested component (helpers, embedded frameworks) carries a consistent signature. Incomplete depth is the classic “works on my Mac, fails elsewhere” source — the main binary passes everywhere while a nested helper trips verification only in stricter contexts.

    Deep verification output confirming all nested components carry consistent signatures
    Step 5 — Depth verified: every nested component signing-consistent · img/m-40-s5.png
  6. Verify end to end in the destination context, not just the build machine: open the app on another Mac (or a clean user account), where Gatekeeper's full assessment applies. Success there — with the verification trio passing locally — closes the loop; record the signing configuration that achieved it so rebuilds reproduce it.

    The signed and notarized app opening cleanly on a second Mac for the first time
    Step 6 — Destination-context verification: the app passes Gatekeeper where it will actually run · img/m-40-s6.png

Verification and next steps

The signing setup is sound when local verification passes, the destination context opens the app without refusal, and the build pipeline reproduces the configuration on every rebuild (signing as a scripted final step, not a manual afterthought). If notarization itself rejects your app, its report names the specific violation (an unsigned nested item, an entitlement problem, an unsupported component) — that report is the next diagnostic input, and it is precise enough to act on. The standing principle: the signature is part of the build, sealed last over final contents, with depth verified — treat it like any other build artifact, because it is one.

41

Fix executable-permission problems in shell scripts and development tools

D · Developer quirks & compatibilityBeginnerIntel & Apple Silicon

“Permission denied” on a script you just wrote is almost always the missing executable bit — a one-command fix that becomes recurring trouble only when the bit is lost in transit (zips, some copy paths) or never committed (git does track it, but only if you set it). This tutorial covers the fix, the variants, and why the same error can mean two different things.

Problem and symptoms

You run a script with its path (./build.sh) and get permission denied — even though the file exists, is readable, and works fine when invoked through the interpreter explicitly (bash build.sh). The asymmetry is the diagnosis: direct execution requires the executable bit; interpreter invocation does not, because the shell reads the file as data rather than executing it.

Unix permissions include a per-class execute bit, and on macOS (as on other Unix systems) a file's ability to run directly depends on it. Scripts are where this bites most, because they are created in editors that do not set the bit for you — a newly written script is readable but not executable until someone says so. The bit also travels (or fails to travel) with files: git records it as part of the file's metadata, archives like zip typically discard it entirely, and some copy operations preserve it while others do not. Each transport explains a different version of “it worked yesterday.”

Likely causes and diagnosis

Three situations produce the same error string, and the file's permission listing distinguishes them. First, the bit was never set: a fresh script — the listing shows no x for your user class, and the fix is a single chmod command. Second, the bit was lost in transit: the script came through a zip archive or a copy path that drops metadata — the listing again lacks the bit, but the history (where the file came from) tells you why, and the same fix applies with the prevention being a transport that preserves permissions (tar archives do; git does). Third, the error is not about the script at all: permission denied can also mean the directory containing the script lacks execute permission for you (directories need the execute bit to be traversable), or — on protected paths — a privacy-layer refusal rather than a Unix one (tutorial 17's distinction). The listing plus the path's character separates these.

Terminal — inspect and fix (one changes the system)
# Read-only: see the script's current permission bits.
ls -l ./build.sh

# Changes the system: add the executable bit for your user.
chmod +x ./build.sh

# In a git repository, record the execute bit so clones keep it:
git update-index --chmod=+x build.sh

The first command only reports; the other two change state (the file's bits, and the repository's recorded metadata respectively).

Step-by-step solution

  1. Confirm which error you actually have: run the script directly and note the exact message, then check its permission listing. No execute bit for your user class is the classic case; a present-but-failing bit points at the directory or privacy layers instead.

    Terminal showing a script's permission listing with no execute bit set
    Step 1 — The diagnosis confirmed: the listing shows what is missing · img/m-41-s1.png
  2. Set the executable bit with the chmod command above. This changes only that file's metadata — nothing about its contents — and takes effect immediately: the direct invocation should now work.

    The chmod command applied, with the script running directly for the first time
    Step 2 — The bit set and the script executing directly · img/m-41-s2.png
  3. If the file came from a repository, make the fix durable: record the execute bit in git so every clone receives it. Without this step, the next person (or your next machine) hits the identical error — the bit must live in the repository's metadata, not just on your disk.

    The execute bit recorded in git so all future clones of the script are executable
    Step 3 — The fix made durable in version control · img/m-41-s3.png
  4. If the file came through an archive or copy that drops permissions, note the transport as the root cause and prefer transports that preserve metadata (tar-based archives, git) for scripts. The per-file chmod remains the immediate fix; the transport change is what prevents recurrence.

    A tar archive being used to transfer scripts with their permissions intact
    Step 4 — The transport corrected: permission-preserving archives for script distribution · img/m-41-s4.png
  5. If the bit is set but direct execution still fails, check the containing directory's permissions (it needs execute/traverse permission for you) and — for paths under system protection or privacy-covered locations — apply tutorial 17's two-layer distinction before changing anything further.

    The containing directory's permissions inspected when the script's own bits are already correct
    Step 5 — The next layer checked: the directory's traverse permission, or the privacy layer · img/m-41-s5.png
  6. Verify with the original invocation (the exact command that failed) and record which situation you had — never-set, lost-in-transit, or different-layer — so the next occurrence is a lookup. For teams, a project convention (scripts committed with the bit set, tar for transfers) closes this whole class at the source.

    The original failing command now succeeding, with the situation type noted in the project's conventions
    Step 6 — Verification and the convention recorded to prevent recurrence · img/m-41-s6.png

Verification and next steps

The fix is verified when the original direct invocation succeeds, fresh clones of the repository receive executable scripts without manual intervention, and your transfer paths preserve the bit. If permission-denied errors keep appearing on files whose bits are correct, stop at this tutorial's edge and move to tutorial 17 — that error string has a second life in the privacy layer, and its fixes (grants to the right application) are entirely different from chmod.

42

Troubleshoot Xcode command-line tools, SDK selection and compiler configuration

D · Developer quirks & compatibilityIntermediateIntel & Apple Silicon

Compiler failures on macOS often trace back to one place: the system's selection of which developer toolchain is active — the standalone command-line tools or a full Xcode installation, and which SDK each build uses. The selection is inspectable and switchable with standard commands, and most “compiler is broken” mysteries resolve once you see what the system actually has pointed where.

Problem and symptoms

The failure family: a build cannot find the compiler or its headers, complains about an unknown SDK, reports a version mismatch between compiler and SDK, or simply stops working after an Xcode update. The common thread is that macOS does not have one fixed compiler — it has a selection mechanism (xcode-select) pointing either at the lightweight command-line tools package or at a full Xcode.app, and every build inherits whatever that pointer says.

Two facts frame the diagnosis. First, the command-line tools are a real, standalone installation — many developers use them without ever installing full Xcode, and nothing in this tutorial assumes Xcode is present. Second, macOS's compiler naming has a quirk worth knowing: the standard C and C++ compiler on the system is Clang, and the familiar gcc name resolves to it (a compatibility alias), so version output that says clang when you asked for gcc is normal behavior, not a misconfiguration.

Likely causes and diagnosis

Diagnosis starts by reading the selection state: where the system's developer tools pointer currently points (the query command reports the active path — the CLT directory or an Xcode.app), what compiler that path provides (its version output), and what SDKs are visible (the list command shows every SDK the active toolchain offers). Mismatches between these three answers locate the problem: a pointer at tools that were removed, an expected SDK absent from the list, or environment variables (the C/C++ compiler selection variables) overriding the system default with something stale.

Terminal — read the toolchain state (all read-only)
# Where does the system's developer-tools pointer point?
xcode-select -p

# What compiler does that path provide?
cc --version | head -2

# Which SDKs are visible to the active toolchain?
xcodebuild -showsdks 2>/dev/null || xcrun --sdk macosx --show-sdk-path

# Is an environment variable overriding the compiler choice?
echo "CC=$CC CXX=$CXX"

All read-only. The four answers — pointer, compiler, SDK list, overrides — together place any toolchain failure in its category.

Step-by-step solution

  1. Run the inspection block and record all four answers. A pointer path that no longer exists (tools removed, Xcode moved) is an immediate finding; a healthy pointer with missing SDKs or stale overrides points at the next specific step.

    Terminal showing the four toolchain inspection answers: pointer path, compiler version, SDK list and environment overrides
    Step 1 — The toolchain state read: pointer, compiler, SDKs and overrides recorded · img/m-42-s1.png
  2. If the tools are missing or the pointer is broken, install or repair the command-line tools package (the standard installation prompt appears when a build first needs them, or can be triggered deliberately). This restores the baseline toolchain for developers who do not use full Xcode.

    The command line tools installation prompt appearing and completing
    Step 2 — The baseline toolchain restored: the CLT package installed or repaired · img/m-42-s2.png
  3. If both CLT and full Xcode exist and behavior changed after an update, check which one the pointer selects and whether that matches your intent. Switching the pointer between them is a deliberate system change (it affects every build on the machine) — make it consciously, note what you switched from, and re-test your builds afterwards.

    The developer tools pointer being switched deliberately between CLT and a full Xcode installation
    Step 3 — The selection made deliberate: the pointer pointed where your workflow intends · img/m-42-s3.png
  4. For SDK-specific failures, compare what the build wants against what the active toolchain lists. An expected SDK missing from the list means the toolchain version predates it (update the toolchain) or the SDK belongs to a different selection (switch per step 3). A present-but-unselected SDK may need explicit specification in the build configuration rather than reliance on defaults.

    The visible SDK list compared against the version a failing build requires
    Step 4 — The SDK gap identified: what the build needs versus what the toolchain offers · img/m-42-s4.png
  5. Check for environment overrides before blaming the system: compiler-selection variables set in your shell (or inherited from a GUI context — tutorial 35's territory) can force a stale compiler path on every build. Clear or correct them where they are set, and verify with a fresh shell that the system defaults now apply.

    A stale compiler environment variable removed from the shell configuration, with a fresh shell confirming clean defaults
    Step 5 — The override found and corrected at its source · img/m-42-s5.png
  6. Verify with a real build: compile a small program (or your project's smallest target) and confirm the compiler, SDK and output all match expectations. Then record the machine's intended toolchain state — which selection is active and why — so the next update or reinstall starts from a known point.

    A test program compiling cleanly with the corrected toolchain selection in place
    Step 6 — The verification build passing, toolchain state documented · img/m-42-s6.png

Verification and next steps

The configuration is sound when a fresh shell (and a GUI-launched context, if your tools run there) resolves the intended compiler and SDKs, and real builds pass. If failures persist with a verified-correct toolchain state, the problem has moved into build-system territory (a project's own compiler flags or SDK assumptions) — diagnose that project's configuration next, using this tutorial's four answers as the known-good baseline. And after any future Xcode or CLT update, re-run step 1: these pointers and lists are exactly the things updates change under your feet.

43

Diagnose environment-variable and PATH differences across Terminal, IDEs and automation

D · Developer quirks & compatibilityIntermediateIntel & Apple Silicon

The same machine can present three different environments: your Terminal's fully configured one, a GUI application's minimal launch environment, and an automation context's own reduced set. When a variable exists in one and not another, the bug is never “the variable vanished” — it is that two contexts were silently compared. This tutorial gives the comparison method and the fix patterns, ranked from explicit to hacky.

Problem and symptoms

The symptom set: a tool finds its dependencies in Terminal but not from an IDE's task runner; a scheduled job fails with missing variables that your interactive session has; a script works when you run it but not when a service runs it. Each case is the same structural problem wearing different clothes — the executing context inherited an environment you did not configure, because no one told it to read your configuration.

The structure (detailed in tutorial 36 for shell files) is that environments are inherited, not discovered: every process receives its parent's environment at launch, and nothing about a variable “exists on the machine” in an absolute sense. Your Terminal's PATH is large because your login shell built it from startup files; a GUI app's PATH is small because its launcher (the system's user session context) never ran those files; an automation job's environment is whatever its scheduler chose to give it. Three parents, three environments — all correct, all different.

Likely causes and diagnosis

The diagnostic method is direct measurement rather than inference: capture the full environment in each suspect context (every context can print its own), diff the captures against each other, and read the differences as your complete list of variables that behave differently between contexts. The diff does not tell you which difference matters — that requires knowing what the failing tool actually reads (its documentation, or simply testing with the variable supplied) — but it guarantees you are not missing a candidate.

The fix patterns rank by explicitness. Best: make the dependency visible where the code runs — the script or task sets its own required variables (or sources a canonical environment file), so it works in any context without depending on inheritance. Good: configure the launcher — many IDEs and schedulers let you define per-task environments, which keeps the fix local to the tool that needs it. Weakest: system-wide session environment changes (the user-level launch-context setter) — they work until reboot, affect every GUI app invisibly, and hide the real dependency instead of naming it. Use them only as a temporary bridge, never as the architecture.

Terminal — capture environments from each context
# In Terminal, capture the shell's environment.
env | sort > ~/env-capture-terminal.txt

# From the IDE/automation context, have it run:
env | sort > ~/env-capture-app.txt

# The differences are your candidate list.
diff ~/env-capture-terminal.txt ~/env-capture-app.txt

All read-only. Lines present only in the Terminal capture are exactly what the other context lacks — every cross-context variable mystery reduces to this diff.

Step-by-step solution

  1. Reproduce the failure in its native context and capture the exact missing-variable error. Note which tool failed, what it was trying to use (a path, a runtime location, a configuration value) — this names the variable class you are hunting.

    An automation job failing with a missing-variable error, captured in its native context
    Step 1 — The failure reproduced natively, with the missing dependency named · img/m-43-s1.png
  2. Capture environments from both contexts using the method above and diff them. Read the diff for variables the failing tool plausibly uses — PATH first, then its specific runtime or configuration variables. Mark each as confirmed-needed (the tool documents it) or suspected.

    The environment diff between Terminal and the automation context, with needed variables marked
    Step 2 — The diff read: candidate missing variables identified and ranked · img/m-43-s2.png
  3. Apply the explicit fix first: add the required variables to the script or task itself (a small header that sets them, or a source of your canonical environment file from tutorial 36). The dependency is now visible in the code that needs it — the most maintainable form the fix can take.

    A script header setting its required environment variables explicitly, independent of its caller
    Step 3 — The explicit fix: the script names and sets its own dependencies · img/m-43-s3.png
  4. If the tool offers per-task environment configuration (most IDEs and schedulers do), set the variables there instead — keeping the fix in the launcher's configuration where it is visible to anyone reading that task. Choose between this and step 3 based on which location your team actually maintains.

    An IDE's task environment settings holding the variables that task needs
    Step 4 — The launcher-level alternative: per-task environment configuration used where it fits · img/m-43-s4.png
  5. Reserve system-wide session changes for emergencies only: if a context cannot be configured and the explicit fix is not possible, the user-level launch-context setter can bridge temporarily — but note that it lasts until reboot, affects all GUI apps, and must be removed once the real fix lands. Treat its use as a flag that something in the architecture needs attention.

    A temporary session-level environment bridge applied with a note marking it for removal
    Step 5 — The last-resort bridge, explicitly marked as temporary · img/m-43-s5.png
  6. Verify in the failing context specifically (not Terminal): re-run the job or task and confirm it now succeeds. Then sweep for siblings — other scripts and tasks with the same invisible inheritance assumption — and give them the same explicit treatment. One diff has revealed a pattern; fix the pattern, not just the instance.

    The failing job now passing in its own context, with sibling tasks queued for the same explicit fix
    Step 6 — Verification in the native context, and the pattern swept across sibling tasks · img/m-43-s6.png

Verification and next steps

The fix is verified when the originally failing context succeeds without any Terminal involvement, and every environment dependency of your scripts and tasks is declared where it can be read. If a GUI application's environment still surprises you after all this, tutorial 35 covers the app-specific side (its own launch configuration); if shell-file questions arise about where a variable should live for interactive use, tutorial 36's file map answers them. The standing rule: environments are inherited, so dependencies must be declared — invisible inheritance is the root of this entire problem class.

44

Troubleshoot symlinks, mounted volumes and path assumptions that misbehave on macOS

D · Developer quirks & compatibilityAdvancedIntel & Apple Silicon

macOS hides its real paths behind symlinks (/tmp is not where it says it is), mounts removable storage at a location that can disappear between sessions, and resolves path strings in ways that tools disagree about. Code written against the visible names — or against another operating system's layout — inherits all three surprises. This tutorial maps the actual layout and the assumptions to check.

Problem and symptoms

The failure shapes: a tool writes to /tmp and later cannot find its own files (they are in a different real location than the name suggests); a script hardcodes a path under /Volumes for an external drive and breaks after a reboot (the mount is gone, or has a different name); two tools given the “same” path disagree about what it refers to (one followed a symlink, the other did not); or a repository cloned from a machine with a different layout carries paths that do not exist here.

The underlying layout facts: macOS presents several well-known short paths — /tmp, /var, /etc and their relatives — as symbolic links to locations under /private. The visible name is real for reading and writing (the system resolves the link transparently), but tools that compare path strings literally, or that resolve symlinks before acting, see the longer real form and can fail to match what other tools report. Separately, external drives mount under /Volumes with names derived from their volume labels — a name that is stable while mounted but not guaranteed across reboots, renames or multiple drives with similar labels.

Likely causes and diagnosis

Diagnosis asks three questions per incident. First: is a symlink involved? Check the path's real form (the canonicalization command resolves all links) and compare it with what the tool expects — a mismatch between the given path and its resolved form is your finding, and the fix is to use one consistent form (usually the resolved one) across the affected code. Second: is a mount point involved? Verify the volume is actually mounted right now (the mounts list shows what exists), and whether your code assumes a specific name — robust code discovers the mount by identifier or label at runtime rather than hardcoding /Volumes/Name. Third: does the assumption come from another system? Paths copied from Linux documentation (/home, /mnt conventions) or from another Mac's layout are assumptions that fail silently until something acts on them.

Terminal — inspect the real layout (all read-only)
# Read-only: what is a path's fully resolved real form?
readlink -f /tmp

# Read-only: which volumes are mounted right now, and where?
mount | grep -i local
ls /Volumes

# Read-only: find symlinks at the top level that hide the real layout
ls -ld /tmp /var /etc

All read-only. The resolved forms and the current mount list are the ground truth against which any path assumption can be checked.

Step-by-step solution

  1. Reproduce the failure with the exact path involved, and capture both forms: the path as given to the tool and its fully resolved real form. If they differ, a symlink is in play — note which components of the path are links (the top-level listing shows the usual suspects).

    Terminal showing a path's given form versus its resolved real form, exposing the symlink difference
    Step 1 — The two forms captured: given path versus resolved path · img/m-44-s1.png
  2. Identify which tool resolves and which does not. Run the failing operation with paths in both forms (the short name and the fully resolved one): if one form works and the other fails, you have found the tool that is sensitive to the difference — its documentation or source will show whether it canonicalizes before acting.

    The same operation tested with the short path form and the resolved form, one succeeding and one failing
    Step 2 — The sensitive tool identified by testing both path forms · img/m-44-s2.png
  3. Standardize on one form in your code and configurations: prefer fully resolved paths (no symlinks in the string) for anything that compares, stores or passes paths between components. This removes the ambiguity at the source rather than patching each tool's behavior individually.

    A configuration updated to use fully resolved paths, eliminating the symlink ambiguity
    Step 3 — The standard applied: resolved paths used consistently across the affected code · img/m-44-s3.png
  4. For external-drive paths, replace hardcoded /Volumes/Name assumptions with runtime discovery: check that the expected volume is mounted (by name or, better, by its unique identifier), and fail with a clear message when it is not — telling the user to connect the drive beats a cryptic file-not-found three steps later.

    A script checking for the expected volume's mount state and reporting a clear message when absent
    Step 4 — The mount assumption made explicit: runtime discovery with a helpful failure message · img/m-44-s4.png
  5. Audit for cross-system path assumptions: search your scripts and configurations for paths that exist only on another machine or operating system (home-directory conventions, mount-point habits, project locations). Each one is a latent failure waiting for the context where it does not hold — replace with discovery or configuration.

    A grep across scripts finding hardcoded paths that assume another machine's layout
    Step 5 — The assumption audit: cross-system path dependencies found and listed · img/m-44-s5.png
  6. Verify the fixes in the contexts where they matter: a fresh reboot (mounts reset), a renamed or reconnected drive, and a clean clone of your repository on another Mac. Path assumptions fail at context boundaries — test at the boundaries, not just in the session where everything was already set up.

    Post-reboot verification: scripts finding their drives and paths correctly after a fresh session
    Step 6 — Boundary testing: reboots, drive changes and clean clones all passing · img/m-44-s6.png

Verification and next steps

The setup is robust when your code uses resolved paths consistently, discovers mounts at runtime instead of assuming them, and survives the boundary tests (reboot, drive reconnect, fresh clone). If a specific tool keeps misbehaving with symlinks despite consistent inputs, that is a property of that tool to report — with both path forms and its behavior documented, which this tutorial's method produces. The general principle transfers to every cross-platform project: the visible name of a path is an interface, not a guarantee; code should either resolve before acting or be explicit about which form it requires.

§07

Category E · 8 tutorials

Diagnostics & workflows

The repeatable layer: reading logs and crash reports, using Activity Monitor for resource hogs, classifying what a problem actually is before fixing it, and documenting incidents so the next one starts from knowledge instead of zero.

45

Use Console and macOS logs to investigate an application failure

E · Diagnostics & workflowsIntermediateIntel & Apple Silicon

When an app crashes or misbehaves, macOS has already recorded the evidence: a crash report with the exception and faulting thread, plus the unified log stream around the moment of failure. Reading those records — instead of guessing from the symptom — turns “it quit” into a specific, reportable finding.

Problem and symptoms

An application crashes (quit unexpectedly, blank screen, vanishing window) or misbehaves in a way its own interface does not explain. The visible symptom is the least informative part of the event: the app's last words are in system records you can read, and for many failures they name the cause directly — an exception type, a faulting function, a resource that ran out.

macOS keeps two complementary record types. Crash reports are generated when a process dies abnormally: each one is a file in your user's diagnostic-reports folder, containing the exception or signal that killed the process, a backtrace of what it was doing (the faulting thread's call stack), and context about the app and system. The unified log is the continuous stream of messages from processes and the system itself — searchable by time range, process name and content — which captures what the app (and everything around it) said in the minutes before the failure, including warnings that never reach the user.

Likely causes and diagnosis

The diagnostic sequence starts with the crash report if one exists: find the newest report for the failing app, read its exception/termination section (the direct cause — a specific exception type or signal), then the faulting thread's backtrace (where in the code it happened). The exception type often categorizes the failure immediately — a null-reference class of error points at application logic; an out-of-memory termination points at resource pressure (cross-check with tutorial 46); a watchdog timeout points at the app hanging rather than crashing. When no crash report exists (the app misbehaved without dying), the unified log is the primary source: query the time window around the incident filtered to the app's process, and read its messages for errors, warnings and the last operations before the problem.

Terminal — query the unified log (read-only)
# Read-only: messages from a specific process in the last 15 minutes.
# Replace AppName with the failing application's process name.
log show --last 15m --predicate 'process == "AppName"' --style compact

# Read-only: list your most recent crash reports (newest first).
ls -lt ~/Library/Logs/DiagnosticReports | head

Both commands only read. The log query shows what the app said before failing; the listing finds the crash report files to open.

Step-by-step solution

  1. Reproduce the failure once, deliberately, and note the exact time. A known incident time makes every later query precise — you are searching a window, not the whole history.

    The application failing on demand, with the exact time of the incident noted beside it
    Step 1 — The failure reproduced on purpose, timestamp recorded · img/m-45-s1.png
  2. Check for a crash report: list your diagnostic-reports folder newest-first and look for an entry matching the app and time. If one exists, open it (it is plain text) — its header section names the exception or signal that terminated the process.

    A crash report file opened in a text editor showing its exception and termination sections
    Step 2 — The crash report found and its cause section read · img/m-45-s2.png
  3. Read the faulting thread's backtrace in the report: the top frames name the functions the app was executing when it died. You do not need to understand every frame — the top few, plus the exception type, are enough to categorize the failure and to hand a vendor or developer a precise description.

    The faulting thread backtrace in a crash report, top frames highlighted for reading
    Step 3 — The backtrace read: where in the code the failure occurred · img/m-45-s3.png
  4. If no crash report exists (misbehavior without a hard crash), query the unified log for the incident window filtered to the app's process, using the command above. Read its messages chronologically: warnings and errors in the final minutes before the problem are usually the story.

    A log show query output showing the app's messages in the minutes before its misbehavior
    Step 4 — The unified log queried: the app's own messages around the incident · img/m-45-s4.png
  5. Cross-check resource context if the report or logs suggest it: an out-of-memory termination pairs with tutorial 46's Activity Monitor findings; a hang pairs with CPU/disk activity from the same window. The log tells you what happened; the resource picture tells you why.

    Activity Monitor's memory pressure viewed alongside a crash report indicating resource exhaustion
    Step 5 — The resource context cross-checked against the log's findings · img/m-45-s5.png
  6. Assemble the finding into a shareable record: app name and version, exception type or key log messages, the top backtrace frames (if any), and the resource context. Redact anything personal in file paths or content before sharing — crash reports contain more of your system than you might expect.

    A redacted incident summary: exception, key frames and resource context ready to share with a vendor
    Step 6 — The finding assembled and redacted, ready for a vendor or developer · img/m-45-s6.png

Verification and next steps

The investigation is complete when you can state the failure's category (application exception, resource exhaustion, hang, external dependency) with the record as evidence — a state that is actionable by whoever fixes it. If the same crash recurs with an identical report, attach that report to a vendor bug report; if reports vary between crashes, collect several and note what differs (inputs, timing, system state) — variation across crashes is itself diagnostic information. For failures where no records exist at all, the next step is forcing more instrumentation: the app's own debug logging, if it has any, or running it under a debugger in development contexts.

46

Use Activity Monitor to find the process consuming your CPU, memory or disk

E · Diagnostics & workflowsBeginnerIntel & Apple Silicon

A slow Mac, a spinning fan, or a disk that never goes quiet all reduce to one question: which process is doing the work? Activity Monitor answers it directly — but only if you read the right columns and know that the memory number everyone looks at first is not the one that matters most.

Problem and symptoms

The machine feels wrong: apps take longer to respond, the fans spin up with nothing obviously running, storage activity never settles, or battery drain accelerates. The instinct is to blame “the computer” — but on a working Mac there is almost always a specific process accounting for the load, and finding it is the difference between a fix and a replacement decision.

Activity Monitor is the system's built-in view of exactly this: every running process with its current resource consumption. Two reading skills matter more than anything else in this tool. First, sort by the resource you are diagnosing — the list's default order shows nothing useful about load; clicking a column header ranks processes by that metric and puts the culprit at the top. Second, read memory pressure, not raw memory percentage: macOS uses idle memory for caches by design, so a high RAM-usage number is often normal; the pressure indicator (green/yellow/red) tells you whether memory is actually constraining the system.

Likely causes and diagnosis

CPU load has a few familiar owners: a legitimate heavy task (a build, a video export, a large search — identifiable by name, best left to finish), a misbehaving application (high sustained CPU with no user-visible work — the candidate for quitting or updating), and background system maintenance (indexing after large changes, Time Machine backups — time-limited and self-resolving). Memory problems show differently: sustained red pressure means something is holding more than the system can cache around; the responsible processes are the ones with the largest memory footprints in the sorted list. Disk load is read from the disk activity columns (per-process read/write rates): a process writing continuously when idle points at runaway logging or sync behavior (tutorial 31's territory once identified).

Step-by-step solution

  1. Open Activity Monitor (via Spotlight search or the Utilities folder) and let the list populate for a few seconds. Do not act yet — the first job is reading, and acting on a stale snapshot is how innocent processes get quit.

    Activity Monitor open with its process list populated, before any sorting
    Step 1 — Activity Monitor open and settling, the full process list visible · img/m-46-s1.png
  2. Sort by the metric matching your symptom: click the CPU column for slowness or fan noise, Memory for pressure problems, or a disk column for storage activity. The top of the sorted list is now your suspect ranking — note the top few names and their values.

    The process list sorted by CPU with the highest consumers at the top
    Step 2 — The list sorted by the symptom's metric, suspects ranked · img/m-46-s2.png
  3. Read the memory pressure graph (the window's bottom bar) before judging any memory number. Green means healthy regardless of the RAM percentage; yellow means getting tight; red means active constraint — only at red (or sustained yellow) is memory actually your problem.

    The memory pressure graph in Activity Monitor showing its current color state
    Step 3 — The pressure graph read: the number that actually matters for memory · img/m-46-s3.png
  4. For a CPU suspect, sample it before quitting: select the process and use the sample action to capture what it is doing — the resulting stack summary shows whether it is in recognizable work (a build loop, a sync) or stuck in something odd. This one step separates “busy legitimately” from “busy pathologically.”

    A process sample report showing what the high-CPU process is actually executing
    Step 4 — The suspect sampled: its activity identified before any action · img/m-46-s4.png
  5. Act according to what you found. A legitimate heavy task: let it finish (note when it should end). A misbehaving app you can live without: quit it normally first; force-quit only if it will not respond, understanding that force-quitting may lose its unsaved work. A system process under load: do not kill it — identify the user activity that triggered it (a backup, an index) and let the maintenance complete.

    A misbehaving application being quit normally after its sample showed pathological activity
    Step 5 — The proportionate action: normal quit, patience, or force-quit as the last resort · img/m-46-s5.png
  6. Verify the effect: watch the sorted list and the pressure graph after your action. The metric should improve measurably — if it does not, the next-ranked suspect is now in view, and the same read-sample-act cycle applies to it. Record what you found (process, metric, outcome) so a recurring pattern becomes recognizable.

    The sorted list after the action showing the load reduced and the machine responsive again
    Step 6 — The verification: the metric improved, the finding recorded · img/m-46-s6.png

Verification and next steps

The diagnosis is verified when the machine's behavior matches the reduced load (responsive apps, quiet fans, settled disk) and you can name what was consuming the resource. If a process keeps returning to the top of the rankings after quitting, it is being relaunched by something — tutorial 48 covers the launch agents and daemons that keep software alive. And if the culprit turns out to be a system process under sustained load with no user activity behind it, that pattern (with your Activity Monitor evidence) belongs in Apple Support's hands rather than in your kill list.

47

Diagnose a Mac that behaves differently in Safe Mode

E · Diagnostics & workflowsIntermediateIntel & Apple Silicon

If a problem disappears in Safe Mode, you have just run the most informative test available on macOS: it tells you the cause is third-party software that Safe Mode excludes. The value is not staying in Safe Mode (it is a reduced-functionality test environment) but using the difference to eliminate suspects one by one back in normal operation.

Problem and symptoms

You have a persistent problem — instability, crashes, slowness, misbehavior — that ordinary troubleshooting has not resolved. You boot into Safe Mode (the method per architecture is in tutorial 09's table), re-test, and the problem is gone or clearly reduced. The question now is what that difference actually tells you, and how to convert it into a fix.

Safe Mode changes a specific, knowable set of conditions: third-party kernel and system extensions do not load, a basic disk check runs at startup, the font cache is rebuilt, and various startup behaviors are skipped. Everything Apple's own system does still runs — so a problem that survives Safe Mode has cleared the third-party-extension suspect list largely in one move, while a problem that vanishes in it has been localized to exactly the layer Safe Mode turned off.

Likely causes and diagnosis

When behavior differs between normal and Safe startup, the cause lives in what Safe Mode excludes — which in practice means one of a small set: a third-party system extension or driver (disk utilities, virtualization tools, security agents, hardware add-ons), a login-time item that misbehaves at startup, or font-cache-related rendering problems (the cache rebuild is why some visual glitches clear after a Safe Mode cycle even if the underlying app remains). The diagnostic strategy is elimination: return to normal operation and remove suspects one at a time — most recently added first — testing after each removal. Removing everything at once “works” but identifies nothing, which defeats the test's entire purpose.

Step-by-step solution

  1. Before leaving Safe Mode, document precisely what differs: which symptoms are gone, which remain (some problems have multiple independent causes — a partial change is still information), and what you can observe in Safe Mode that you cannot normally. This record frames the elimination phase.

    A written comparison of symptoms in normal startup versus Safe Mode, noting which changed
    Step 1 — The difference documented: symptom by symptom, normal versus Safe Mode · img/m-47-s1.png
  2. Return to normal startup and inventory the third-party layer: recently installed apps and system utilities, anything providing extensions or drivers (check the system-extension approval history in Privacy & Security for what is registered), and login items. Order this list by recency — newest first is the highest-probability elimination order.

    The third-party inventory: recently added apps, system extensions and login items listed by date
    Step 2 — The suspect list built and ordered by recency · img/m-47-s2.png
  3. Remove or disable the first suspect only: quit its processes, remove it from login items if present, and (for uninstallable software) uninstall it cleanly. One change per test — this discipline is what makes the result interpretable. Then reboot normally and re-run the original failing scenario.

    The most recent suspect disabled and the Mac rebooted into normal mode for the first elimination test
    Step 3 — The first elimination: one suspect removed, normal boot, original test re-run · img/m-47-s3.png
  4. Read the result. If the problem is gone: you have found the cause — reinstall the item only if you need it (and watch for the problem's return, which would confirm the finding beyond doubt). If the problem persists: the suspect is cleared, and the next one in the recency order takes its place. Repeat until the cause is isolated or the list is exhausted.

    The elimination log: suspects tested, results recorded, the causal item identified
    Step 4 — The elimination sequence logged until the causal item is isolated · img/m-47-s4.png
  5. If the suspect list exhausts without a finding, widen the test: some startup behavior Safe Mode skips lives in user-level launch agents rather than installed apps — tutorial 48's inspection methods apply here. Also re-test after a few normal days: intermittent problems may need more than one boot to reveal which item triggers them.

    Launch agents being reviewed as the widened suspect pool after the app list was exhausted
    Step 5 — The search widened: launch agents and multi-day observation in play · img/m-47-s5.png
  6. Close the loop with a clean state: the causal item removed or replaced (a newer version may fix it — check before deciding to live without it), the system back in normal operation, and the finding recorded with the full elimination trail. That trail is your evidence if the problem ever returns in a new form.

    The Mac in normal operation after the fix, with the complete elimination record saved
    Step 6 — Normal operation restored, the full diagnostic trail preserved · img/m-47-s6.png

Verification and next steps

The diagnosis is complete when normal-mode behavior matches expectations across several days (not just the first reboot after the fix), and you can name the item whose removal changed the outcome. If the causal software is essential and no fixed version exists, that is a vendor conversation — with your elimination record as proof of causation, which is far more persuasive than symptom descriptions alone. And if problems persist in both normal and Safe Mode, the third-party layer has been largely exonerated: the next diagnostic territory is system-level (disk health, resource pressure per tutorial 46) or hardware (Apple Diagnostics), per tutorial 09's decision table.

48

Investigate startup and background-task problems: launch agents and daemons

E · Diagnostics & workflowsAdvancedIntel & Apple Silicon

Software that restarts itself after you quit it, login that takes forever, or a background process you cannot account for are all managed by the same system: launchd and its agent/daemon definitions. Learning to read those definitions — where they live, what they declare, and how to disable one cleanly — turns invisible background behavior into inspectable configuration.

Problem and symptoms

The symptom family: an application relaunches after you quit it; your Mac takes an unusually long time to reach the desktop after login; a process runs that no open application explains; or a background service fails silently and its dependent features stop working. In each case, something is starting software at boot or login on a schedule or in response to events — and the definitions of those behaviors are files you can read.

The architecture has two levels with different scopes. Launch agents run in a user's context: they start when that user logs in (or on other triggers their definition specifies), and live in per-user locations for single users or system-wide locations for all users. Launch daemons run as the system itself, from boot before any login — background services with no user session attached. Each is defined by a property-list file declaring what to run, when (at load, on a schedule, in response to events), and whether to keep it alive if it dies. A process you cannot explain is almost always declared in one of these files.

Likely causes and diagnosis

Diagnosis proceeds from observation to definition. First, identify the process: Activity Monitor (tutorial 46) names it; its full path (visible in the process details) often identifies the vendor or product immediately. Second, find its definition: search the agent and daemon directories for a file whose declared program matches the process — the directories are small and grep-able, and the match is usually unambiguous. Third, read what the definition declares: the program path (what runs), the triggers (why it starts — at login, on a schedule, when a condition occurs), and the keep-alive setting (whether it respawns after death — the answer to “why does it come back?”).

Terminal — inspect launchd definitions (read-only)
# Read-only: list the user's loaded launch jobs.
launchctl list

# Read-only: find which definition files mention a suspect program name.
grep -l "SuspectProgram" ~/Library/LaunchAgents/*.plist \
  /Library/LaunchAgents/*.plist /Library/LaunchDaemons/*.plist 2>/dev/null

# Read-only: inspect one definition's declared behavior.
plutil -p /path/to/the-definition.plist

All read-only. The listing shows what is loaded; the search finds a suspect's definition; the inspection prints its full declared behavior in readable form.

Step-by-step solution

  1. Capture the misbehaving process precisely: its name, full executable path (from Activity Monitor's details), and when it appears (at login? continuously? after you quit something?). The path is your search key for the next step.

    Activity Monitor showing a background process with its full executable path in the details pane
    Step 1 — The process identified with its full path, ready to trace to a definition · img/m-48-s1.png
  2. Search the three definition directories for a file declaring that program (the grep command above). A match tells you both what starts it and whose scope it is in (user agent vs. system-wide agent vs. daemon) — which determines what disabling it affects.

    The grep finding the definition file that declares the suspect program
    Step 2 — The definition found: which file declares this process, and in whose scope · img/m-48-s2.png
  3. Read the definition's full contents (the plutil command prints it readably). Note the triggers and especially any keep-alive setting — that flag is why the process returns after being quit, and whether it is present changes what “disabling” must do.

    A launchd definition printed in readable form showing its program, triggers and keep-alive setting
    Step 3 — The behavior decoded: what runs, when it starts, whether it respawns · img/m-48-s3.png
  4. Decide the disposition with eyes open. Apple's own definitions (identifiable by their paths and vendors) should not be disabled without understanding what you are turning off — many “mystery processes” are system services doing legitimate work. Third-party definitions from software you installed can be evaluated against whether you still want that software's background behavior at all.

    The decision recorded: a third-party background service evaluated against whether its function is still wanted
    Step 4 — The disposition decided deliberately, Apple-owned services left untouched · img/m-48-s4.png
  5. Disable what you have decided to disable, cleanly: for user agents, the modern unload command (booting the job out of your user session) stops it without deleting anything; the definition file remains in place, so re-enabling is trivial if the decision needs revisiting. Deletion of third-party definitions is a stronger step — take it only with the software's own uninstaller or when you are certain the item is orphaned.

    A user launch agent unloaded cleanly, its definition file preserved for possible re-enablement
    Step 5 — The clean disable: job unloaded, definition preserved, nothing deleted · img/m-48-s5.png
  6. Verify across a reboot: the disabled process should not return after a restart (a keep-alive definition that still loads will reveal itself immediately). Confirm the dependent functionality you expected to lose is actually gone — and note it, so future “why does X not work” questions have their answer recorded.

    Post-reboot verification: the disabled process absent, with the expected functionality change noted
    Step 6 — The reboot test passed, consequences documented · img/m-48-s6.png

Verification and next steps

The investigation is complete when every background process you care about has a known definition and a deliberate disposition (kept, disabled, or removed with the software), and reboots confirm the state holds. If a process appears with no matching definition anywhere, it is being started by another running program rather than launchd — trace its parent in Activity Monitor to find the spawner. And if login slowness persists after cleaning up agents, measure where the time actually goes (the system's own startup reporting) before removing more items — some delays are disk or network conditions, not software count.

49

Build a non-destructive diagnostic report for a support request

E · Diagnostics & workflowsIntermediateIntel & Apple Silicon

Support requests succeed or fail on the quality of their evidence. A diagnostic report assembled from read-only commands — system identity, disk state, top processes, recent crash reports, targeted log excerpts — gives the other side everything they need to start working instead of asking you to re-describe symptoms. Every command in this workflow changes nothing on your machine.

Problem and symptoms

You have a problem that needs more eyes than yours — a vendor's support team, Apple Support, or a colleague — and the usual first exchange (symptoms described in words, questions asked, answers given, more questions) is slow because nothing verifiable exists on the other side. A machine-generated report collapses that loop: it states the facts about your system as they are right now, in a form that can be checked rather than recalled.

The design principle is non-destruction: every command used here reads state and writes only to a file you create. No settings change, no files deleted, no services restarted. That property matters twice — it keeps the report safe to produce under time pressure, and it means the system's state at report time is genuinely the state that was reported (no diagnostic step has altered what it describes).

Likely causes and diagnosis

The report covers five fact families, each answering a question support will ask anyway. Identity: which macOS version, which hardware model, which chip — the questions that determine which documentation and tools apply. Capacity: disk free space and the largest consumers — because a surprising share of “software” problems have capacity as a contributing factor. Load: what was consuming resources at report time — process names with their usage, so “the Mac is slow” becomes “process X at 90% CPU.” Failures: recent crash reports for the affected software — the exception types and backtraces that categorize crashes (tutorial 45's reading method applies to whoever receives them). Context: a bounded log excerpt around the incident window, filtered to the relevant process — what the system recorded, without dumping gigabytes of unrelated history.

Diagnostic report commands: purpose, whether they modify data, and how to interpret their output
CommandPurposeModifies data?Reading the output
sw_versmacOS version and buildNoThe exact release support should reference; note the build number too
sysctl -n hw.model and uname -mHardware model identifier and architectureNoModel code for hardware-specific documentation; arm64 vs x86_64 for architecture-dependent issues
df -h /Startup disk free spaceNoThe conservative container-level figure (tutorial 25); low free space is a common contributing factor
top -l 1 | head -30Snapshot of top resource consumersNoProcess names and usage at report time; one sample is a snapshot, not an average — say so in the report
ls -lt ~/Library/Logs/DiagnosticReports | headMost recent crash reportsNoFilenames encode app and date; attach the newest relevant file's contents, redacted
log show --last 30m --predicate 'process == "AppName"' --style compactBounded log excerpt for one processNoThe app's own messages in the window; bound the time range so output stays reviewable

Step-by-step solution

  1. Create the report file and record the incident framing first: what is broken, when it started, what was tried. This human-written header orients every fact that follows — a report without context is just data.

    A new text file opened with the incident summary written at its top before any commands run
    Step 1 — The report started: incident context written before the machine facts · img/m-49-s1.png
  2. Capture the identity section: run the version and hardware commands, appending their output to the report. These two or three lines determine which documentation applies to everything else in the file.

    Terminal running sw_vers and the hardware model commands with output appended to the report file
    Step 2 — The identity facts captured: version, model, architecture · img/m-49-s2.png
  3. Capture capacity and load: the disk free-space figure and a top-process snapshot. Note the time of the snapshot in the report — load figures are moment-in-time, and the timestamp tells the reader how much weight to give them.

    The disk space and top-process commands running, their output timestamped into the report
    Step 3 — Capacity and load snapshotted with their time noted · img/m-49-s3.png
  4. Attach the failure evidence: list recent crash reports, select the newest one matching the affected application, and append its contents (the exception section and faulting thread are the load-bearing parts — but the full file is fine at this stage). If no crash report exists, say so explicitly in the report; absence of evidence is itself a fact.

    The most recent relevant crash report appended to the diagnostic file
    Step 4 — The crash evidence attached, or its absence stated · img/m-49-s4.png
  5. Append the bounded log excerpt for the affected process over the incident window (the table's command pattern). Keep the window tight — thirty minutes around the incident, not a day — so the excerpt stays readable by whoever has to work from it.

    A bounded log show query for the affected process, its compact output appended to the report
    Step 5 — The log context captured within a tight, stated time window · img/m-49-s5.png
  6. Redact before sharing: scan the assembled file for personal data that the commands swept in incidentally — file paths containing names, account identifiers, document titles inside log lines. Replace or remove what does not need to be there; the technical facts survive redaction intact. Then send the single file with your incident summary attached.

    The assembled report reviewed for personal data before being sent as a single file
    Step 6 — The report redacted and ready: one file, complete evidence, no unnecessary personal data · img/m-49-s6.png

Verification and next steps

The report is complete when a reader who knows nothing about your machine can state: what system it is, how much space it has, what was loaded, what failed (with exception evidence), and what the affected process said around the incident — without asking you a single clarifying question. That is the test to apply before sending. When support responds with follow-up requests for more specific data, extend the same method: bounded queries, read-only commands, one file. The habit generalizes beyond support tickets — the same report format is how you document problems for your future self (tutorial 52's record builds on it).

50

Create a safe pre-update maintenance checklist for a developer's Mac

E · Diagnostics & workflowsBeginnerIntel & Apple Silicon

A developer's Mac carries more state than a general-purpose one: toolchains with version-sensitive components, uncommitted work, external drives with project data, and caches that double as capacity problems. A pre-update checklist converts the upgrade from an event you hope goes well into a procedure whose steps are verifiable before the first click — this is tutorial 01's preparation, specialized for development machines.

Problem and symptoms

This checklist exists because developer upgrades fail in specific, preventable ways: an update lands on top of uncommitted work that a later rollback would lose; a toolchain silently changes version or architecture behavior and the first discovery is a broken build days later; an external drive with project data was mid-backup when the upgrade restarted the machine. None of these are dramatic failures — they are gaps between “I updated my Mac” and “I verified what the update did to everything I depend on.”

The checklist's structure follows the risk order: first, make rollback real (backup verification — an unverified backup is not a safety net); second, make work loss impossible (commit and push); third, create the comparison baseline (record what you have, so after the update you can say what changed); fourth, protect the peripherals (external drives disconnected or confirmed backed up before a restart); fifth, clear the capacity risk (free space for download and staging). Each step has a verifiable completion state — the checklist is done when every line has evidence, not intention.

Likely causes and diagnosis

There is no fault to diagnose here; the “diagnosis” is the pre-state assessment each step performs. The backup step verifies that a restore path exists (recent successful backup, browsable contents). The work step verifies that nothing uncommitted exists anywhere (every repository clean and pushed — including the scratch projects people forget about). The baseline step captures the facts an upgrade can change: toolchain versions (language runtimes, package managers, build tools), the active developer-tools selection (tutorial 42's pointer), and disk free space. The peripheral step establishes which external drives are connected and whether their contents are current elsewhere. Each verification produces a yes/no with evidence — and a single no stops the upgrade until it becomes yes.

Step-by-step solution

  1. Verify the backup is real: open Time Machine (or your equivalent), confirm a recent successful backup exists, and browse into it to see current files. “The backup runs nightly” is not verification — “I opened last night's backup and saw this week's files” is.

    Time Machine's browser opened on a recent backup, showing current project files inside it
    Step 1 — The backup verified by browsing its actual contents, not just its schedule · img/m-50-s1.png
  2. Make work loss impossible: in every repository you have open (and the ones you almost forgot), commit outstanding changes and push to a remote. Check for untracked files that matter — new scripts, configuration, notes live outside version control until someone adds them.

    A terminal showing git status clean and pushed across the working repositories before the update
    Step 2 — All repositories committed and pushed: nothing uncommitted anywhere · img/m-50-s2.png
  3. Capture the toolchain baseline: record versions of your language runtimes, package managers and build tools (one command per tool, output saved to a file), plus the active developer-tools selection. This is the document you will diff against after the update — without it, “what changed?” has no answer.

    A baseline file accumulating version outputs from each development tool before the upgrade
    Step 3 — The baseline captured: every toolchain version recorded for post-update comparison · img/m-50-s3.png
  4. Handle external drives: disconnect any not needed during the update, or confirm their contents are backed up elsewhere before a restart touches them. A drive mid-copy when the machine reboots is exactly the kind of gap this checklist exists to close.

    An external project drive safely ejected before the update begins
    Step 4 — External drives accounted for: disconnected or confirmed current · img/m-50-s4.png
  5. Clear the capacity risk: check free space against the update's needs (tutorial 01's standard), and if it is tight, run the low-risk cleanup batch first (caches and build artifacts — tutorials 29–30) rather than starting the upgrade on a disk that may not have room to stage.

    The storage view showing free space confirmed adequate for download and staging headroom
    Step 5 — Free space verified, with a low-risk cleanup run first if it was tight · img/m-50-s5.png
  6. Run the update, then verify against your baseline: new version confirmed, toolchain versions diffed against step 3's file (anything that changed gets a note), a representative build run to confirm the toolchain works, and any system-extension approval prompts consciously decided. The checklist is complete when every line has its post-update evidence.

    The post-update diff against the baseline file with a representative build passing
    Step 6 — Post-update verification: versions diffed, a build run, approvals decided · img/m-50-s6.png

Verification and next steps

The procedure is verified when the machine runs the new version with a working toolchain (proven by a real build, not an assumption), every baseline difference accounted for, and the backup confirmed as the rollback path if anything later proves unacceptable. Keep the completed checklist file — it is both the record of this upgrade and the template for the next one, with the step list identical and only the recorded values changing. If the post-update diff reveals toolchain changes that break something, those are now specific, named differences to fix (or report) rather than a vague sense that “the update broke my setup.”

51

Distinguish a temporary macOS issue from an app bug or a persistent configuration problem

E · Diagnostics & workflowsIntermediateIntel & Apple Silicon

Every Mac problem falls into one of three ownership classes: a transient system state that clears on its own or with a reboot, an application defect that follows the app across conditions, or a persistent configuration that reproduces reliably in your specific setup. The class determines everything — what to test, what to report, and whether any fix is even yours to make. This tutorial is the decision procedure that assigns the class.

Problem and symptoms

The problem presents as misbehavior with an unclear owner: something fails, slows, or misbehaves, and the first question — “is this macOS, the app, or my configuration?” — is the one that determines every subsequent action. Guess the class wrong and you waste effort in the wrong direction: rebooting forever for an app bug, reporting a vendor defect that is actually your own setting, or reconfiguring a system whose state was simply transient.

The three classes have distinguishable behaviors, and the distinguishing tests are cheap. A transient state (a stuck process, a one-time resource spike, a background task mid-run) clears with time or a reboot and does not reliably reproduce. An application bug reproduces in the same app across conditions — different times, after reboots, often for other users of that app version — while other software behaves normally. A persistent configuration problem reproduces reliably in your setup specifically: a particular setting, permission state, or environment that makes the failure deterministic on this machine.

Likely causes and diagnosis

The decision procedure works by applying discriminating tests in an order that maximizes information per test. The reboot test comes first because it is free and splits transient from persistent in one observation. The cross-condition test (does the failure follow the app, or stay with this machine?) separates app bugs from configuration problems. And the isolation tests — another user account on the same machine, Safe Mode for the third-party layer — narrow the configuration class to its specific owner. Each test's result routes you to the next one or to a conclusion; the matrix below is the complete map.

Troubleshooting decision matrix: observations, what they suggest, and the next useful test
ObservationSuggestsNext useful test
Failure clears after a reboot and does not return for daysTransient system stateWatch for recurrence; if it returns, re-run this matrix with the recurrence's timing noted
Failure reproduces immediately after reboot, every timePersistent (configuration or app)Cross-condition test: does the same failure occur in a different app doing similar work?
Only one app fails; equivalents work fineApplication bug (or that app's own configuration)Test the app under a fresh user account — if it still fails, the defect travels with the app; report to vendor with evidence (tutorial 45/49)
Fails for one user on the machine, works for anotherUser-level configuration (that user's settings, permissions, environment)Compare the two users' relevant state: permission lists (category B), shell/environment (tutorial 36/43)
Fails for all users, only on this machineMachine-level configuration or system stateSafe Mode test (tutorial 09/47): if it clears, the third-party layer is implicated; if not, system-level investigation (disk health, resources)
Fails on this machine and other machines with the same setupShared configuration or a version-specific app/system interactionBisect the shared setup: change one common element at a time across test machines

One meta-rule governs the whole procedure: each test changes exactly one condition. Rebooting and updating an app in the same cycle, or switching networks while also clearing caches, produces uninterpretable results — the procedure's power comes from single-variable observations, which is why it looks slower than shotgun fixes and arrives at answers they never do.

Step-by-step solution

  1. Characterize the failure before testing: what exactly fails, how to trigger it (the minimal reproduction steps), when it first appeared, and whether it is constant or intermittent. A failure you cannot reliably trigger cannot be classified — so step one is making it triggerable on demand, even if only sometimes.

    A written failure characterization: the minimal steps to reproduce, first-seen date and constancy noted
    Step 1 — The failure characterized with reproducible trigger steps · img/m-51-s1.png
  2. Run the reboot test: restart normally and re-attempt the exact reproduction. Record the outcome precisely — gone, reduced, or unchanged. This single observation splits transient from persistent and sets the rest of the route.

    The reproduction attempted after a clean reboot, with the result recorded against the pre-reboot behavior
    Step 2 — The reboot test run: transient versus persistent split in one observation · img/m-51-s2.png
  3. If persistent, run the cross-condition test: does an equivalent app performing similar work fail the same way? A failure isolated to one app points at that app (or its per-app configuration); a failure shared across apps points at the machine or user layer beneath them.

    An equivalent application tested for the same failure, isolating which layer owns the problem
    Step 3 — The cross-condition test: one app versus several, naming the owning layer · img/m-51-s3.png
  4. Narrow with isolation tests as the matrix directs: a fresh user account separates user-level from machine-level configuration; Safe Mode separates the third-party layer from the system core. Each test is one condition changed, one observation recorded, one branch of the matrix followed.

    The failure tested under a fresh user account, its result feeding the decision matrix's next branch
    Step 4 — The isolation tests: fresh account and Safe Mode observations recorded · img/m-51-s4.png
  5. Assign the class from the accumulated observations, using the matrix as the map. State it explicitly in your notes: “transient state, cleared by reboot” / “application defect, reproduces across conditions” / “user-level configuration, located at [specific setting or grant].” A named class is an actionable conclusion; a vague impression is not.

    The classification written down: the failure's ownership class stated with the observations supporting it
    Step 5 — The class assigned and stated, with its supporting observations · img/m-51-s5.png
  6. Act on the class. Transient: monitor for recurrence (and record what preceded it if it returns). App bug: assemble evidence per tutorials 45 and 49 and report to the vendor — fixing it is not your job, proving it is. Configuration: apply the specific fix in the layer you located (permission per category B, environment per tutorial 36/43, setting per its own documentation), then re-run the original reproduction to confirm.

    The class-appropriate action taken: vendor report filed or configuration fixed, with the original test re-run passing
    Step 6 — The class-appropriate action completed and verified against the original reproduction · img/m-51-s6.png

Verification and next steps

The procedure is complete when the failure's class is stated with its evidence, and the class-appropriate action has been taken and verified against the original reproduction. If the classification ever feels forced — observations not cleanly matching a row — that is information too: it usually means two independent problems are interleaved, and the right move is to split them (one per matrix run) rather than stretch a single explanation. The standing value of this procedure is habit: with each use, the observation vocabulary gets faster, and problems that once took days of shotgun fixes take an afternoon of single-variable tests.

52

Create a repeatable troubleshooting record that actually gets used

E · Diagnostics & workflowsBeginnerIntel & Apple Silicon

The difference between troubleshooting that compounds and troubleshooting that repeats is a record: a consistent document of what the symptom was, what changed recently, what was tested with what result, and what finally verified as fixed. This tutorial provides the template and the discipline around it — short enough to fill in under pressure, structured enough that six-months-later-you can read it without archaeology.

Problem and symptoms

The problem this solves is not any single technical failure — it is the pattern of re-diagnosing: the same class of problem returning months later with all the knowledge lost, support conversations restarting from zero because nothing was written down, and fixes applied whose rationale no one (including you) can reconstruct. Every troubleshooting session produces knowledge that evaporates when the tab closes; a record is how that knowledge survives.

The design constraint is honesty about when records get written: under time pressure, with the problem still active, by someone who wants to fix things rather than document them. A template that requires an essay will not be filled in; one that requires five short sections will. Each section below exists because its absence causes a specific future failure — the symptom section because “it was acting weird” is undiagnosable later; the recent-changes section because half of all “mysterious” problems have an identifiable trigger that nobody noted at the time; the tests-and-results section because without it, future sessions repeat tests already run (and worse, re-run fixes already tried and rejected).

Likely causes and diagnosis

There is no fault to diagnose — this is a workflow adoption. The “diagnosis” it performs on each incident is the same classification tutorial 51 produces (transient, app defect, or configuration), now captured in a durable form. What the record adds beyond that moment is the longitudinal view: the recent-changes section, filled in honestly over many incidents, becomes a map of what actually breaks your setup — which updates, which installs, which configuration changes correlate with failures. That map is worth more than any single fix, because it predicts where the next problem will come from.

Template — copy and fill per incident
MAC TROUBLESHOOTING RECORD
Date / machine (model + macOS version):

SYMPTOM (what was observed, verbatim where possible)
  -
RECENT CHANGES (updates, installs, config edits, hardware — last 2 weeks)
  -
CLASSIFICATION (transient state | app defect | configuration — per tutorial 51)
  -
TESTS AND RESULTS (one line each: what was tested, what happened)
  1.
FIX APPLIED (the specific change that resolved it, and where)
  -
VERIFICATION (how the fix was confirmed against the original symptom)
  -
STILL UNRESOLVED / ESCALATED TO (if applicable, with evidence attached)
  -

Five minutes to fill when an incident closes. The copy button grabs the whole template; keep a blank copy in your notes app for the next time.

Step-by-step solution

  1. Start the record at the incident's beginning, not its end: fill the date/machine line and the symptom section while the observation is fresh. Two or three lines of verbatim symptom (the exact error text, the exact behavior) are what make everything later possible — “it was slow” versus “app X took 40 seconds to launch after login, three days in a row” are different documents.

    A new record started with the date, machine identity and verbatim symptom lines filled in first
    Step 1 — The record opened at the incident's start, symptom captured verbatim · img/m-52-s1.png
  2. Fill the recent-changes section before diagnosing further: the last two weeks of updates, installs, configuration edits and hardware changes on this machine. This is the section people skip and regret — it is where half the triggers live, and it takes a minute to list honestly.

    The recent-changes section filled in: updates, installs and config edits from the past two weeks
    Step 2 — The change history recorded before diagnosis deepens · img/m-52-s2.png
  3. Work the incident using the library's methods (the classification procedure, the specific tutorials the symptom points to), and log each test as a one-liner in the tests section as you run it: what was tested, what happened. Resist batching — the discipline of writing each result down is what prevents re-running dead ends.

    The tests-and-results section growing one line per test as the diagnosis proceeds
    Step 3 — Every test logged as it happens, results recorded immediately · img/m-52-s3.png
  4. Close the record when the incident resolves: state the fix applied (the specific change, in the specific place), and — critically — the verification: how you confirmed the fix against the original symptom, not a related one. A fix verified against the wrong test is an open incident wearing a closed label.

    The fix and verification sections completed: the specific change and how it was confirmed against the original symptom
    Step 4 — The record closed: fix stated, verification against the original symptom documented · img/m-52-s4.png
  5. File it where you will find it: one consistent location (a notes folder, a document per machine) with the date in the filename. The record's value is proportional to its retrievability — a perfect record nobody finds is no record at all.

    Completed records filed by date in one consistent location for the machine
    Step 5 — The record filed consistently, findable by future sessions · img/m-52-s5.png
  6. When a similar problem returns (it will), open the old record first. The recent-changes pattern across your records tells you what actually breaks this setup; the tests section saves you the dead ends; and if you are escalating to support, the record is the report — extend it with tutorial 49's machine facts and send it.

    A returning problem being checked against an old record, its change history revealing the recurring trigger
    Step 6 — The compounding effect: a recurrence met with prior knowledge instead of amnesia · img/m-52-s6.png

Verification and next steps

The practice is working when three things hold: records get filled in under pressure without resentment (the template is short enough), a returning problem starts from an old record rather than zero, and at least one support conversation has been shortened by sending a record instead of describing symptoms. The record format deliberately mirrors tutorial 49's support report — they are the same document at different stages, which means every closed incident is already half a support ticket when you need one. That is the compounding: each five-minute habit payment buys faster diagnoses for the rest of the machine's life.

Pre-update maintenance checklist

The preparation that makes an upgrade a procedure instead of a gamble — the same order as tutorial 50, with its verification points. Ticks are saved in this browser only; nothing about your Mac is sent anywhere.

Stored locally in this browser — nothing is sent anywhere.